Browse Source

Merge pull request #12707 from AndriiLandiak/feature/calculated-fields-vc

VC support for Calculated fields
pull/12841/head
Viacheslav Klimov 2 years ago
committed by GitHub
parent
commit
0966aea2d0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  2. 1
      application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java
  3. 21
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java
  5. 6
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java
  6. 57
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  7. 9
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
  8. 6
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java
  9. 10
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java
  11. 5
      application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java
  12. 1
      application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java
  13. 80
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  14. 128
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  15. 26
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java
  16. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java
  17. 1
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java
  18. 14
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java
  19. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java
  20. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java
  21. 5
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java
  22. 1
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java
  23. 4
      common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
  24. 3
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  25. 1
      dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java
  26. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  27. 10
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java
  28. 1
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java
  29. 4
      dao/src/main/resources/sql/schema-entities.sql
  30. 14
      dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
  31. 3
      ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html
  32. 14
      ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts
  33. 3
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html
  34. 7
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts
  35. 3
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html
  36. 7
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts
  37. 3
      ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html
  38. 6
      ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts
  39. 3
      ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html
  40. 6
      ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts
  41. 7
      ui-ngx/src/app/shared/models/vc.models.ts
  42. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
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.ie.EntityImportResult; import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -61,6 +62,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
private final Map<EntityType, EntityImportService<?, ?, ?>> importServices = new HashMap<>(); private final Map<EntityType, EntityImportService<?, ?, ?>> importServices = new HashMap<>();
private final RelationService relationService; private final RelationService relationService;
private final CalculatedFieldService calculatedFieldService;
private final RateLimitService rateLimitService; private final RateLimitService rateLimitService;
private final TbLogEntityActionService logEntityActionService; private final TbLogEntityActionService logEntityActionService;
@ -72,7 +74,6 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE
); );
@Override @Override
public <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException { public <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {
if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) { if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) {
@ -129,13 +130,11 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
} }
} }
@Override @Override
public Comparator<EntityType> getEntityTypeComparatorForImport() { public Comparator<EntityType> getEntityTypeComparatorForImport() {
return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf); return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <I extends EntityId, E extends ExportableEntity<I>, D extends EntityExportData<E>> EntityExportService<I, E, D> getExportService(EntityType entityType) { private <I extends EntityId, E extends ExportableEntity<I>, D extends EntityExportData<E>> EntityExportService<I, E, D> getExportService(EntityType entityType) {
EntityExportService<?, ?, ?> exportService = exportServices.get(entityType); EntityExportService<?, ?, ?> exportService = exportServices.get(entityType);

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

@ -32,7 +32,6 @@ public interface EntitiesExportImportService {
<E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(EntitiesImportCtx ctx, EntityExportData<E> exportData) throws ThingsboardException; <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(EntitiesImportCtx ctx, EntityExportData<E> exportData) throws ThingsboardException;
void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException; void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException;
Comparator<EntityType> getEntityTypeComparatorForImport(); Comparator<EntityType> getEntityTypeComparatorForImport();

21
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.AttributeScope;
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;
import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.cf.CalculatedField;
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.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.sync.ie.AttributeExportData; import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService; import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
@ -59,6 +61,8 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
private RelationDao relationDao; private RelationDao relationDao;
@Autowired @Autowired
private AttributesService attributesService; private AttributesService attributesService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Override @Override
public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException { public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {
@ -98,6 +102,10 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
Map<String, List<AttributeExportData>> attributes = exportAttributes(ctx, entity); Map<String, List<AttributeExportData>> attributes = exportAttributes(ctx, entity);
exportData.setAttributes(attributes); exportData.setAttributes(attributes);
} }
if (ctx.getSettings().isExportCalculatedFields()) {
List<CalculatedField> calculatedFields = exportCalculatedFields(ctx, entity.getId());
exportData.setCalculatedFields(calculatedFields);
}
} }
private List<EntityRelation> exportRelations(EntitiesExportCtx<?> ctx, E entity) throws ThingsboardException { private List<EntityRelation> exportRelations(EntitiesExportCtx<?> ctx, E entity) throws ThingsboardException {
@ -141,6 +149,19 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
return attributes; return attributes;
} }
private List<CalculatedField> exportCalculatedFields(EntitiesExportCtx<?> ctx, EntityId entityId) {
List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entityId);
calculatedFields.forEach(calculatedField -> {
calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, entityId));
calculatedField.getConfiguration().getArguments().values().forEach(argument -> {
if (argument.getRefEntityId() != null) {
argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId()));
}
});
});
return calculatedFields;
}
protected <ID extends EntityId> ID getExternalIdOrElseInternal(EntitiesExportCtx<?> ctx, ID internalId) { protected <ID extends EntityId> ID getExternalIdOrElseInternal(EntitiesExportCtx<?> ctx, ID internalId) {
if (internalId == null || internalId.isNullUid()) return internalId; if (internalId == null || internalId.isNullUid()) return internalId;
var result = ctx.getExternalId(internalId); var result = ctx.getExternalId(internalId);

6
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java

@ -47,7 +47,11 @@ public class AssetImportService extends BaseEntityImportService<AssetId, Asset,
@Override @Override
protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData<Asset> exportData, IdProvider idProvider) { protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData<Asset> exportData, IdProvider idProvider) {
return assetService.saveAsset(asset); Asset savedAsset = assetService.saveAsset(asset);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, savedAsset, exportData, idProvider);
}
return savedAsset;
} }
@Override @Override

6
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java

@ -50,7 +50,11 @@ public class AssetProfileImportService extends BaseEntityImportService<AssetProf
@Override @Override
protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData, IdProvider idProvider) { protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData, IdProvider idProvider) {
return assetProfileService.saveAssetProfile(assetProfile); AssetProfile saved = assetProfileService.saveAssetProfile(assetProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, saved, exportData, idProvider);
}
return saved;
} }
@Override @Override

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

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.HasDefaultOption;
import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
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.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -50,6 +51,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.sync.ie.AttributeExportData; import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
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.ie.EntityImportResult; import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.action.EntityActionService;
@ -78,6 +80,8 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
@Lazy @Lazy
private ExportableEntitiesService entitiesService; private ExportableEntitiesService entitiesService;
@Autowired @Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired
private RelationService relationService; private RelationService relationService;
@Autowired @Autowired
private RelationDao relationDao; private RelationDao relationDao;
@ -134,7 +138,7 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
} }
protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) { protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
return false; return importCalculatedFields(ctx, prepared, exportData, idProvider);
} }
@Override @Override
@ -278,12 +282,59 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}); });
} }
protected boolean importCalculatedFields(EntitiesImportCtx ctx, E savedEntity, D exportData, IdProvider idProvider) {
if (exportData.getCalculatedFields() == null || !ctx.isSaveCalculatedFields()) {
return false;
}
boolean updated = false;
List<CalculatedField> existing = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), savedEntity.getId());
List<CalculatedField> fieldsToSave = exportData.getCalculatedFields().stream()
.peek(calculatedField -> {
calculatedField.setTenantId(ctx.getTenantId());
calculatedField.setEntityId(savedEntity.getId());
calculatedField.getConfiguration().getArguments().values().forEach(argument -> {
if (argument.getRefEntityId() != null) {
argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt()));
}
});
}).toList();
for (CalculatedField existingField : existing) {
boolean found = fieldsToSave.stream().anyMatch(importedField -> compareCalculatedFields(existingField, importedField));
if (!found) {
calculatedFieldService.deleteCalculatedField(ctx.getTenantId(), existingField.getId());
updated = true;
}
}
for (CalculatedField calculatedField : fieldsToSave) {
boolean found = existing.stream().anyMatch(existingField -> compareCalculatedFields(existingField, calculatedField));
if (!found) {
calculatedFieldService.save(calculatedField);
updated = true;
}
}
return updated;
}
private boolean compareCalculatedFields(CalculatedField existingField, CalculatedField newField) {
CalculatedField oldCopy = new CalculatedField(existingField);
CalculatedField newCopy = new CalculatedField(newField);
oldCopy.setId(null);
newCopy.setId(null);
oldCopy.setVersion(null);
newCopy.setVersion(null);
oldCopy.setCreatedTime(0);
newCopy.setCreatedTime(0);
return oldCopy.equals(newCopy);
}
protected void onEntitySaved(User user, E savedEntity, E oldEntity) throws ThingsboardException { protected void onEntitySaved(User user, E savedEntity, E oldEntity) throws ThingsboardException {
logEntityActionService.logEntityAction(user.getTenantId(), savedEntity.getId(), savedEntity, null, logEntityActionService.logEntityAction(user.getTenantId(), savedEntity.getId(), savedEntity, null,
oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, user); oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, user);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected E findExistingEntity(EntitiesImportCtx ctx, E entity, IdProvider idProvider) { protected E findExistingEntity(EntitiesImportCtx ctx, E entity, IdProvider idProvider) {
return (E) Optional.ofNullable(entitiesService.findEntityByTenantIdAndExternalId(ctx.getTenantId(), entity.getId())) return (E) Optional.ofNullable(entitiesService.findEntityByTenantIdAndExternalId(ctx.getTenantId(), entity.getId()))
@ -313,10 +364,10 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
.orElseThrow(() -> new MissingEntityException(externalId)); .orElseThrow(() -> new MissingEntityException(externalId));
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@RequiredArgsConstructor @RequiredArgsConstructor
protected class IdProvider { protected class IdProvider {
private final EntitiesImportCtx ctx; private final EntitiesImportCtx ctx;
private final EntityImportResult<E> importResult; private final EntityImportResult<E> importResult;

9
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java

@ -64,13 +64,18 @@ public class DeviceImportService extends BaseEntityImportService<DeviceId, Devic
@Override @Override
protected Device saveOrUpdate(EntitiesImportCtx ctx, Device device, DeviceExportData exportData, IdProvider idProvider) { protected Device saveOrUpdate(EntitiesImportCtx ctx, Device device, DeviceExportData exportData, IdProvider idProvider) {
Device savedDevice;
if (exportData.getCredentials() != null && ctx.isSaveCredentials()) { if (exportData.getCredentials() != null && ctx.isSaveCredentials()) {
exportData.getCredentials().setId(null); exportData.getCredentials().setId(null);
exportData.getCredentials().setDeviceId(null); exportData.getCredentials().setDeviceId(null);
return deviceService.saveDeviceWithCredentials(device, exportData.getCredentials()); savedDevice = deviceService.saveDeviceWithCredentials(device, exportData.getCredentials());
} else { } else {
return deviceService.saveDevice(device); savedDevice = deviceService.saveDevice(device);
} }
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, savedDevice, exportData, idProvider);
}
return savedDevice;
} }
@Override @Override

6
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java

@ -52,7 +52,11 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
@Override @Override
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider) { protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider) {
return deviceProfileService.saveDeviceProfile(deviceProfile); DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, saved, exportData, idProvider);
}
return saved;
} }
@Override @Override

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

@ -94,7 +94,6 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.Futures.transform; import static com.google.common.util.concurrent.Futures.transform;
import static org.thingsboard.server.common.data.sync.vc.VcUtils.checkBranchName; import static org.thingsboard.server.common.data.sync.vc.VcUtils.checkBranchName;
@ -304,6 +303,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.updateRelations(config.isLoadRelations()) .updateRelations(config.isLoadRelations())
.saveAttributes(config.isLoadAttributes()) .saveAttributes(config.isLoadAttributes())
.saveCredentials(config.isLoadCredentials()) .saveCredentials(config.isLoadCredentials())
.saveCalculatedFields(config.isLoadCalculatedFields())
.findExistingByName(false) .findExistingByName(false)
.build()); .build());
ctx.setFinalImportAttempt(true); ctx.setFinalImportAttempt(true);
@ -327,7 +327,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
var sw = TbStopWatch.create("before"); var sw = TbStopWatch.create("before");
List<EntityType> entityTypes = request.getEntityTypes().keySet().stream() List<EntityType> entityTypes = request.getEntityTypes().keySet().stream()
.sorted(exportImportService.getEntityTypeComparatorForImport()).collect(Collectors.toList()); .sorted(exportImportService.getEntityTypeComparatorForImport()).toList();
for (EntityType entityType : entityTypes) { for (EntityType entityType : entityTypes) {
log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType); log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType);
sw.startNew("Entities " + entityType.name()); sw.startNew("Entities " + entityType.name());
@ -362,6 +362,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.updateRelations(config.isLoadRelations()) .updateRelations(config.isLoadRelations())
.saveAttributes(config.isLoadAttributes()) .saveAttributes(config.isLoadAttributes())
.saveCredentials(config.isLoadCredentials()) .saveCredentials(config.isLoadCredentials())
.saveCalculatedFields(config.isLoadCalculatedFields())
.findExistingByName(config.isFindExistingEntityByName()) .findExistingByName(config.isFindExistingEntityByName())
.build(); .build();
} }
@ -471,7 +472,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
} }
@Override @Override
public ListenableFuture<EntityDataDiff> compareEntityDataToVersion(User user, EntityId entityId, String versionId) throws Exception { public ListenableFuture<EntityDataDiff> compareEntityDataToVersion(User user, EntityId entityId, String versionId) {
HasId<EntityId> entity = exportableEntitiesService.findEntityByTenantIdAndId(user.getTenantId(), entityId); HasId<EntityId> entity = exportableEntitiesService.findEntityByTenantIdAndId(user.getTenantId(), entityId);
if (!(entity instanceof ExportableEntity)) throw new IllegalArgumentException("Unsupported entity type"); if (!(entity instanceof ExportableEntity)) throw new IllegalArgumentException("Unsupported entity type");
@ -484,6 +485,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.exportRelations(otherVersion.hasRelations()) .exportRelations(otherVersion.hasRelations())
.exportAttributes(otherVersion.hasAttributes()) .exportAttributes(otherVersion.hasAttributes())
.exportCredentials(otherVersion.hasCredentials()) .exportCredentials(otherVersion.hasCredentials())
.exportCalculatedFields(otherVersion.hasCalculatedFields())
.build()); .build());
EntityExportData<?> currentVersion; EntityExportData<?> currentVersion;
try { try {
@ -503,7 +505,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override @Override
public ListenableFuture<List<BranchInfo>> listBranches(TenantId tenantId) throws Exception { public ListenableFuture<List<BranchInfo>> listBranches(TenantId tenantId) {
return gitServiceQueue.listBranches(tenantId); return gitServiceQueue.listBranches(tenantId);
} }

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

@ -69,6 +69,7 @@ public abstract class EntitiesExportCtx<R extends VersionCreateRequest> {
.exportRelations(config.isSaveRelations()) .exportRelations(config.isSaveRelations())
.exportAttributes(config.isSaveAttributes()) .exportAttributes(config.isSaveAttributes())
.exportCredentials(config.isSaveCredentials()) .exportCredentials(config.isSaveCredentials())
.exportCalculatedFields(config.isSaveCalculatedFields())
.build(); .build();
} }
@ -85,4 +86,5 @@ public abstract class EntitiesExportCtx<R extends VersionCreateRequest> {
log.debug("[{}][{}] Local cache put: {}", internalId.getEntityType(), internalId.getId(), externalId); log.debug("[{}][{}] Local cache put: {}", internalId.getEntityType(), internalId.getId(), externalId);
externalIdMap.put(internalId, externalId != null ? externalId : internalId); externalIdMap.put(internalId, externalId != null ? externalId : internalId);
} }
} }

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

@ -91,6 +91,10 @@ public class EntitiesImportCtx {
return getSettings().isSaveCredentials(); return getSettings().isSaveCredentials();
} }
public boolean isSaveCalculatedFields() {
return getSettings().isSaveCalculatedFields();
}
public EntityId getInternalId(EntityId externalId) { public EntityId getInternalId(EntityId externalId) {
var result = externalToInternalIdMap.get(externalId); var result = externalToInternalIdMap.get(externalId);
log.debug("[{}][{}] Local cache {} for id", externalId.getEntityType(), externalId.getId(), result != null ? "hit" : "miss"); log.debug("[{}][{}] Local cache {} for id", externalId.getEntityType(), externalId.getId(), result != null ? "hit" : "miss");
@ -140,5 +144,4 @@ public class EntitiesImportCtx {
return notFoundIds.contains(externalId); return notFoundIds.contains(externalId);
} }
} }

1
application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java

@ -39,6 +39,7 @@ public class EntityTypeExportCtx extends EntitiesExportCtx<VersionCreateRequest>
.exportRelations(config.isSaveRelations()) .exportRelations(config.isSaveRelations())
.exportAttributes(config.isSaveAttributes()) .exportAttributes(config.isSaveAttributes())
.exportCredentials(config.isSaveCredentials()) .exportCredentials(config.isSaveCredentials())
.exportCalculatedFields(config.isSaveCalculatedFields())
.build(); .build();
this.overwrite = ObjectUtils.defaultIfNull(config.getSyncStrategy(), defaultSyncStrategy) == SyncStrategy.OVERWRITE; this.overwrite = ObjectUtils.defaultIfNull(config.getSyncStrategy(), defaultSyncStrategy) == SyncStrategy.OVERWRITE;
} }

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

@ -43,6 +43,15 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
@ -70,6 +79,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.sync.ie.DeviceExportData;
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.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;
@ -79,6 +89,7 @@ import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProfileService;
@ -145,6 +156,8 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
protected TenantService tenantService; protected TenantService tenantService;
@Autowired @Autowired
protected EntityViewService entityViewService; protected EntityViewService entityViewService;
@Autowired
protected CalculatedFieldService calculatedFieldService;
protected TenantId tenantId1; protected TenantId tenantId1;
protected User tenantAdmin1; protected User tenantAdmin1;
@ -191,6 +204,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1");
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1");
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId());
Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(),
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId())
@ -198,6 +212,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
try { try {
return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder()
.exportCredentials(false) .exportCredentials(false)
.exportCalculatedFields(true)
.build()); .build());
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -245,7 +260,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
verify(entityActionService, Mockito.never()).logEntityAction(any(), eq(importedAsset.getId()), eq(importedAsset), verify(entityActionService, Mockito.never()).logEntityAction(any(), eq(importedAsset.getId()), eq(importedAsset),
any(), eq(ActionType.UPDATED), isNull()); any(), eq(ActionType.UPDATED), isNull());
EntityExportData<Asset> updatedAssetEntity = getAndClone(entitiesExportData, EntityType.ASSET); EntityExportData<Asset> updatedAssetEntity = getAndClone(entitiesExportData, EntityType.ASSET);
updatedAssetEntity.getEntity().setLabel("t" + updatedAssetEntity.getEntity().getLabel()); updatedAssetEntity.getEntity().setLabel("t" + updatedAssetEntity.getEntity().getLabel());
Asset updatedAsset = importEntity(tenantAdmin2, updatedAssetEntity).getSavedEntity(); Asset updatedAsset = importEntity(tenantAdmin2, updatedAssetEntity).getSavedEntity();
@ -268,10 +282,27 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)); importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE));
verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice)); verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice));
// calculated field of imported device:
List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId());
assertThat(calculatedFields.size()).isOne();
var importedCalculatedField = calculatedFields.get(0);
assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName());
verify(tbClusterService).onCalculatedFieldUpdated(eq(importedCalculatedField), isNull(), any());
EntityExportData<Device> updatedDeviceEntity = getAndClone(entitiesExportData, EntityType.DEVICE); EntityExportData<Device> updatedDeviceEntity = getAndClone(entitiesExportData, EntityType.DEVICE);
updatedDeviceEntity.getEntity().setLabel("t" + updatedDeviceEntity.getEntity().getLabel()); updatedDeviceEntity.getEntity().setLabel("t" + updatedDeviceEntity.getEntity().getLabel());
Device updatedDevice = importEntity(tenantAdmin2, updatedDeviceEntity).getSavedEntity(); Device updatedDevice = importEntity(tenantAdmin2, updatedDeviceEntity).getSavedEntity();
verify(tbClusterService).onDeviceUpdated(eq(updatedDevice), eq(importedDevice)); verify(tbClusterService).onDeviceUpdated(eq(updatedDevice), eq(importedDevice));
// update calculated field:
DeviceExportData deviceExportData = (DeviceExportData) getAndClone(entitiesExportData, EntityType.DEVICE);
deviceExportData.setCalculatedFields(deviceExportData.getCalculatedFields().stream().peek(field -> field.setName("t_" + field.getName())).toList());
importEntity(tenantAdmin2, deviceExportData).getSavedEntity();
calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId());
assertThat(calculatedFields.size()).isOne();
importedCalculatedField = calculatedFields.get(0);
assertThat(importedCalculatedField.getName()).startsWith("t_");
} }
@Test @Test
@ -290,12 +321,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1"); Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1");
EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1"); EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1");
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId());
Map<EntityId, EntityId> ids = new HashMap<>(); Map<EntityId, EntityId> ids = new HashMap<>();
for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(), for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(),
deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) {
EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId); EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId);
EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder() EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder()
.saveCredentials(false) .saveCredentials(false)
.saveCalculatedFields(true)
.build()); .build());
ids.put(entityId, (EntityId) importResult.getSavedEntity().getId()); ids.put(entityId, (EntityId) importResult.getSavedEntity().getId());
} }
@ -325,10 +359,16 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId()); assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId());
assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId()); assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId());
Device exportedDevice = (Device) exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())).getEntity(); EntityExportData<Device> entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId()));
Device exportedDevice = entityExportData.getEntity();
assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId());
assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId()); assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId());
List<CalculatedField> calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields();
assertThat(calculatedFields.size()).isOne();
CalculatedField field = calculatedFields.get(0);
assertThat(field.getName()).isEqualTo(calculatedField.getName());
EntityView exportedEntityView = (EntityView) exportEntity(tenantAdmin2, (EntityViewId) ids.get(entityView.getId())).getEntity(); EntityView exportedEntityView = (EntityView) exportEntity(tenantAdmin2, (EntityViewId) ids.get(entityView.getId())).getEntity();
assertThat(exportedEntityView.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedEntityView.getCustomerId()).isEqualTo(customer.getId());
assertThat(exportedEntityView.getEntityId()).isEqualTo(device.getId()); assertThat(exportedEntityView.getEntityId()).isEqualTo(device.getId());
@ -340,7 +380,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
deviceProfileService.saveDeviceProfile(importedDeviceProfile); deviceProfileService.saveDeviceProfile(importedDeviceProfile);
} }
protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) { protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) {
Device device = new Device(); Device device = new Device();
device.setTenantId(tenantId); device.setTenantId(tenantId);
@ -549,9 +588,43 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
return relation; return relation;
} }
private CalculatedField createCalculatedField(TenantId tenantId, EntityId entityId, EntityId referencedEntityId) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setTenantId(tenantId);
calculatedField.setEntityId(entityId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId));
calculatedField.setVersion(1L);
return calculatedFieldService.save(calculatedField);
}
private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument();
argument.setRefEntityId(referencedEntityId);
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);
config.setArguments(Map.of("T", argument));
config.setExpression("T - (100 - H) / 5");
Output output = new Output();
output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
return config;
}
protected <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(User user, I entityId) throws Exception { protected <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(User user, I entityId) throws Exception {
return exportEntity(user, entityId, EntityExportSettings.builder() return exportEntity(user, entityId, EntityExportSettings.builder()
.exportCredentials(true) .exportCredentials(true)
.exportCalculatedFields(true)
.build()); .build());
} }
@ -562,6 +635,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
protected <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(User user, EntityExportData<E> exportData) throws Exception { protected <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(User user, EntityExportData<E> exportData) throws Exception {
return importEntity(user, exportData, EntityImportSettings.builder() return importEntity(user, exportData, EntityImportSettings.builder()
.saveCredentials(true) .saveCredentials(true)
.saveCalculatedFields(true)
.build()); .build());
} }

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

@ -44,6 +44,15 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.data.DeviceData;
@ -562,6 +571,81 @@ public class VersionControlTest extends AbstractControllerTest {
checkImportedEntity(tenantId1, defaultDeviceProfile, tenantId2, importedDeviceProfile); checkImportedEntity(tenantId1, defaultDeviceProfile, tenantId2, importedDeviceProfile);
} }
@Test
public void testVcWithCalculatedFields_betweenTenants() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
Device device = createDevice(null, null, "Device 1", "test1");
CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId());
String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
loginTenant2();
loadVersion(versionId, config -> {
config.setLoadCredentials(false);
}, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
Asset importedAsset = findAsset(asset.getName());
Device importedDevice = findDevice(device.getName());
checkImportedEntity(tenantId1, device, tenantId2, importedDevice);
checkImportedEntity(tenantId1, asset, tenantId2, importedAsset);
List<CalculatedField> importedCalculatedFields = findCalculatedFieldsByEntityId(importedDevice.getId());
assertThat(importedCalculatedFields).size().isOne();
assertThat(importedCalculatedFields.get(0)).satisfies(importedField -> {
assertThat(importedField.getName()).isEqualTo(calculatedField.getName());
assertThat(importedField.getType()).isEqualTo(calculatedField.getType());
assertThat(importedField.getId()).isNotEqualTo(calculatedField.getId());
});
}
@Test
public void testVcWithReferencedCalculatedFields_betweenTenants() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
Device device = createDevice(null, null, "Device 1", "test1");
CalculatedField deviceCalculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId());
CalculatedField assetCalculatedField = createCalculatedField("CalculatedField2", asset.getId(), device.getId());
String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
loginTenant2();
loadVersion(versionId, config -> {
config.setLoadCredentials(false);
}, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
Asset importedAsset = findAsset(asset.getName());
Device importedDevice = findDevice(device.getName());
checkImportedEntity(tenantId1, device, tenantId2, importedDevice);
checkImportedEntity(tenantId1, asset, tenantId2, importedAsset);
List<CalculatedField> importedDeviceCalculatedFields = findCalculatedFieldsByEntityId(importedDevice.getId());
assertThat(importedDeviceCalculatedFields).size().isOne();
assertThat(importedDeviceCalculatedFields.get(0)).satisfies(importedField -> {
assertThat(importedField.getName()).isEqualTo(deviceCalculatedField.getName());
assertThat(importedField.getType()).isEqualTo(deviceCalculatedField.getType());
assertThat(importedField.getId()).isNotEqualTo(deviceCalculatedField.getId());
});
List<CalculatedField> importedAssetCalculatedFields = findCalculatedFieldsByEntityId(importedAsset.getId());
assertThat(importedAssetCalculatedFields).size().isOne();
assertThat(importedAssetCalculatedFields.get(0)).satisfies(importedField -> {
assertThat(importedField.getName()).isEqualTo(assetCalculatedField.getName());
assertThat(importedField.getType()).isEqualTo(assetCalculatedField.getType());
assertThat(importedField.getId()).isNotEqualTo(assetCalculatedField.getId());
});
}
@Test
public void testVcWithCalculatedFields_sameTenant() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
CalculatedField calculatedField = createCalculatedField("CalculatedField", asset.getId(), asset.getId());
String versionId = createVersion("asset and field", EntityType.ASSET);
loadVersion(versionId, EntityType.ASSET);
CalculatedField importedCalculatedField = findCalculatedFieldByEntityId(asset.getId());
assertThat(importedCalculatedField.getId()).isEqualTo(calculatedField.getId());
assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(importedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration());
assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType());
}
private <E extends ExportableEntity<?> & HasTenantId> void checkImportedEntity(TenantId tenantId1, E initialEntity, TenantId tenantId2, E importedEntity) { private <E extends ExportableEntity<?> & HasTenantId> void checkImportedEntity(TenantId tenantId1, E initialEntity, TenantId tenantId2, E importedEntity) {
assertThat(initialEntity.getTenantId()).isEqualTo(tenantId1); assertThat(initialEntity.getTenantId()).isEqualTo(tenantId1);
assertThat(importedEntity.getTenantId()).isEqualTo(tenantId2); assertThat(importedEntity.getTenantId()).isEqualTo(tenantId2);
@ -626,10 +710,10 @@ public class VersionControlTest extends AbstractControllerTest {
request.setEntityTypes(Arrays.stream(entityTypes).collect(Collectors.toMap(t -> t, entityType -> { request.setEntityTypes(Arrays.stream(entityTypes).collect(Collectors.toMap(t -> t, entityType -> {
EntityTypeVersionCreateConfig config = new EntityTypeVersionCreateConfig(); EntityTypeVersionCreateConfig config = new EntityTypeVersionCreateConfig();
config.setAllEntities(true); config.setAllEntities(true);
config.setSaveRelations(true); config.setSaveRelations(true);
config.setSaveAttributes(true); config.setSaveAttributes(true);
config.setSaveCredentials(true); config.setSaveCredentials(true);
config.setSaveCalculatedFields(true);
return config; return config;
}))); })));
@ -695,6 +779,7 @@ public class VersionControlTest extends AbstractControllerTest {
config.setLoadAttributes(true); config.setLoadAttributes(true);
config.setLoadRelations(true); config.setLoadRelations(true);
config.setLoadCredentials(true); config.setLoadCredentials(true);
config.setLoadCalculatedFields(true);
config.setRemoveOtherEntities(false); config.setRemoveOtherEntities(false);
config.setFindExistingEntityByName(true); config.setFindExistingEntityByName(true);
configModifier.accept(config); configModifier.accept(config);
@ -941,6 +1026,38 @@ public class VersionControlTest extends AbstractControllerTest {
return doPost("/api/v2/relation", relation, EntityRelation.class); return doPost("/api/v2/relation", relation, EntityRelation.class);
} }
private CalculatedField createCalculatedField(String name, EntityId entityId, EntityId referencedEntityId) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName(name);
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId));
calculatedField.setVersion(1L);
return doPost("/api/calculatedField", calculatedField, CalculatedField.class);
}
private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument();
argument.setRefEntityId(referencedEntityId);
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);
config.setArguments(Map.of("T", argument));
config.setExpression("T - (100 - H) / 5");
Output output = new Output();
output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
return config;
}
protected void checkImportedRuleChainData(RuleChain initialRuleChain, RuleChainMetaData initialMetaData, RuleChain importedRuleChain, RuleChainMetaData importedMetaData) { protected void checkImportedRuleChainData(RuleChain initialRuleChain, RuleChainMetaData initialMetaData, RuleChain importedRuleChain, RuleChainMetaData importedMetaData) {
assertThat(importedRuleChain.getType()).isEqualTo(initialRuleChain.getType()); assertThat(importedRuleChain.getType()).isEqualTo(initialRuleChain.getType());
assertThat(importedRuleChain.getName()).isEqualTo(initialRuleChain.getName()); assertThat(importedRuleChain.getName()).isEqualTo(initialRuleChain.getName());
@ -995,11 +1112,18 @@ public class VersionControlTest extends AbstractControllerTest {
private RuleChain findRuleChain(String name) throws Exception { private RuleChain findRuleChain(String name) throws Exception {
return doGetTypedWithPageLink("/api/ruleChains?", new TypeReference<PageData<RuleChain>>() {}, new PageLink(100, 0, name)).getData().get(0); return doGetTypedWithPageLink("/api/ruleChains?", new TypeReference<PageData<RuleChain>>() {}, new PageLink(100, 0, name)).getData().get(0);
} }
private RuleChainMetaData findRuleChainMetaData(RuleChainId ruleChainId) throws Exception { private RuleChainMetaData findRuleChainMetaData(RuleChainId ruleChainId) throws Exception {
return doGet("/api/ruleChain/" + ruleChainId + "/metadata", RuleChainMetaData.class); return doGet("/api/ruleChain/" + ruleChainId + "/metadata", RuleChainMetaData.class);
} }
private CalculatedField findCalculatedFieldByEntityId(EntityId entityId) throws Exception {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields?", new TypeReference<PageData<CalculatedField>>() {}, new PageLink(100, 0)).getData().get(0);
}
private List<CalculatedField> findCalculatedFieldsByEntityId(EntityId entityId) throws Exception {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields?", new TypeReference<PageData<CalculatedField>>() {}, new PageLink(100, 0)).getData();
}
} }

26
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java

@ -23,7 +23,6 @@ import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasDebugSettings; import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
@ -37,11 +36,14 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
@Schema @Schema
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, ExportableEntity<CalculatedFieldId>, HasDebugSettings { public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings {
@Serial
private static final long serialVersionUID = 4491966747773381420L; private static final long serialVersionUID = 4491966747773381420L;
private TenantId tenantId; private TenantId tenantId;
@ -66,9 +68,6 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
@Getter @Getter
@Setter @Setter
private Long version; private Long version;
@Getter
@Setter
private CalculatedFieldId externalId;
public CalculatedField() { public CalculatedField() {
super(); super();
@ -78,7 +77,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
super(id); super(id);
} }
public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version, CalculatedFieldId externalId) { public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version) {
this.tenantId = tenantId; this.tenantId = tenantId;
this.entityId = entityId; this.entityId = entityId;
this.type = type; this.type = type;
@ -86,7 +85,19 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
this.configurationVersion = configurationVersion; this.configurationVersion = configurationVersion;
this.configuration = configuration; this.configuration = configuration;
this.version = version; this.version = version;
this.externalId = externalId; }
public CalculatedField(CalculatedField calculatedField) {
super(calculatedField);
this.tenantId = calculatedField.tenantId;
this.entityId = calculatedField.entityId;
this.type = calculatedField.type;
this.name = calculatedField.name;
this.debugMode = calculatedField.debugMode;
this.debugSettings = calculatedField.debugSettings;
this.configurationVersion = calculatedField.configurationVersion;
this.configuration = calculatedField.configuration;
this.version = calculatedField.version;
} }
@Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.") @Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.")
@ -112,7 +123,6 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
.append(", configurationVersion=").append(configurationVersion) .append(", configurationVersion=").append(configurationVersion)
.append(", configuration=").append(configuration) .append(", configuration=").append(configuration)
.append(", version=").append(version) .append(", version=").append(version)
.append(", externalId=").append(externalId)
.append(", createdTime=").append(createdTime) .append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']') .append(", id=").append(id).append(']')
.toString(); .toString();

3
common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java

@ -20,11 +20,13 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID; import java.util.UUID;
@Schema @Schema
public class CalculatedFieldId extends UUIDBased implements EntityId { public class CalculatedFieldId extends UUIDBased implements EntityId {
@Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@JsonCreator @JsonCreator
@ -41,4 +43,5 @@ public class CalculatedFieldId extends UUIDBased implements EntityId {
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD; return EntityType.CALCULATED_FIELD;
} }
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java

@ -38,4 +38,5 @@ public class DeviceExportData extends EntityExportData<Device> {
public boolean hasCredentials() { public boolean hasCredentials() {
return credentials != null; return credentials != null;
} }
} }

14
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java

@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import lombok.Data; import lombok.Data;
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;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.sync.JsonTbEntity; import org.thingsboard.server.common.data.sync.JsonTbEntity;
@ -55,6 +56,8 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
public static final Comparator<AttributeExportData> attrComparator = Comparator public static final Comparator<AttributeExportData> attrComparator = Comparator
.comparing(AttributeExportData::getKey).thenComparing(AttributeExportData::getLastUpdateTs); .comparing(AttributeExportData::getKey).thenComparing(AttributeExportData::getLastUpdateTs);
public static final Comparator<CalculatedField> calculatedFieldsComparator = Comparator.comparing(CalculatedField::getName);
@JsonProperty(index = 2) @JsonProperty(index = 2)
@JsonTbEntity @JsonTbEntity
private E entity; private E entity;
@ -65,6 +68,9 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
private List<EntityRelation> relations; private List<EntityRelation> relations;
@JsonProperty(index = 101) @JsonProperty(index = 101)
private Map<String, List<AttributeExportData>> attributes; private Map<String, List<AttributeExportData>> attributes;
@JsonProperty(index = 102)
@JsonIgnoreProperties({"id", "entityId", "createdTime", "version"})
private List<CalculatedField> calculatedFields;
public EntityExportData<E> sort() { public EntityExportData<E> sort() {
if (relations != null && !relations.isEmpty()) { if (relations != null && !relations.isEmpty()) {
@ -73,6 +79,9 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
if (attributes != null && !attributes.isEmpty()) { if (attributes != null && !attributes.isEmpty()) {
attributes.values().forEach(list -> list.sort(attrComparator)); attributes.values().forEach(list -> list.sort(attrComparator));
} }
if (calculatedFields != null && !calculatedFields.isEmpty()) {
calculatedFields.sort(calculatedFieldsComparator);
}
return this; return this;
} }
@ -96,4 +105,9 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
return relations != null; return relations != null;
} }
@JsonIgnore
public boolean hasCalculatedFields() {
return calculatedFields != null;
}
} }

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

@ -25,7 +25,10 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
@Builder @Builder
public class EntityExportSettings { public class EntityExportSettings {
private boolean exportRelations; private boolean exportRelations;
private boolean exportAttributes; private boolean exportAttributes;
private boolean exportCredentials; private boolean exportCredentials;
private boolean exportCalculatedFields;
} }

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

@ -25,8 +25,11 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
@Builder @Builder
public class EntityImportSettings { public class EntityImportSettings {
private boolean findExistingByName; private boolean findExistingByName;
private boolean updateRelations; private boolean updateRelations;
private boolean saveAttributes; private boolean saveAttributes;
private boolean saveCredentials; private boolean saveCredentials;
private boolean saveCalculatedFields;
} }

5
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java

@ -17,13 +17,18 @@ package org.thingsboard.server.common.data.sync.vc.request.create;
import lombok.Data; import lombok.Data;
import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
@Data @Data
public class VersionCreateConfig implements Serializable { public class VersionCreateConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1223723167716612772L; private static final long serialVersionUID = 1223723167716612772L;
private boolean saveRelations; private boolean saveRelations;
private boolean saveAttributes; private boolean saveAttributes;
private boolean saveCredentials; private boolean saveCredentials;
private boolean saveCalculatedFields;
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java

@ -23,5 +23,6 @@ public class VersionLoadConfig {
private boolean loadRelations; private boolean loadRelations;
private boolean loadAttributes; private boolean loadAttributes;
private boolean loadCredentials; private boolean loadCredentials;
private boolean loadCalculatedFields;
} }

4
common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.aware.TenantAwareMsg; import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg; import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
import java.io.Serial;
import java.util.Optional; import java.util.Optional;
/** /**
@ -33,6 +34,8 @@ import java.util.Optional;
*/ */
@Data @Data
public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
@Serial
private static final long serialVersionUID = -5303421482781273062L; private static final long serialVersionUID = -5303421482781273062L;
private final TenantId tenantId; private final TenantId tenantId;
@ -66,4 +69,5 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
public MsgType getMsgType() { public MsgType getMsgType() {
return MsgType.COMPONENT_LIFE_CYCLE_MSG; return MsgType.COMPONENT_LIFE_CYCLE_MSG;
} }
} }

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

@ -135,7 +135,7 @@ public class ProtoUtils {
builder.setName(msg.getName()); builder.setName(msg.getName());
} }
if (msg.getOldName() != null) { if (msg.getOldName() != null) {
builder.setName(msg.getOldName()); builder.setOldName(msg.getOldName());
} }
return builder.build(); return builder.build();
} }
@ -171,7 +171,6 @@ public class ProtoUtils {
return entityTypeByProtoNumber[entityType.getNumber()]; return entityTypeByProtoNumber[entityType.getNumber()];
} }
public static TransportProtos.ToEdgeSyncRequestMsgProto toProto(ToEdgeSyncRequest request) { public static TransportProtos.ToEdgeSyncRequestMsgProto toProto(ToEdgeSyncRequest request) {
return TransportProtos.ToEdgeSyncRequestMsgProto.newBuilder() return TransportProtos.ToEdgeSyncRequestMsgProto.newBuilder()
.setTenantIdMSB(request.getTenantId().getId().getMostSignificantBits()) .setTenantIdMSB(request.getTenantId().getId().getMostSignificantBits())

1
dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.dao.cf; package org.thingsboard.server.dao.cf;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -729,7 +729,6 @@ public class ModelConstants {
public static final String CALCULATED_FIELD_CONFIGURATION_VERSION = "configuration_version"; public static final String CALCULATED_FIELD_CONFIGURATION_VERSION = "configuration_version";
public static final String CALCULATED_FIELD_CONFIGURATION = "configuration"; public static final String CALCULATED_FIELD_CONFIGURATION = "configuration";
public static final String CALCULATED_FIELD_VERSION = "version"; public static final String CALCULATED_FIELD_VERSION = "version";
public static final String CALCULATED_FIELD_EXTERNAL_ID = "external_id";
/** /**
* Calculated field links constants. * Calculated field links constants.

10
dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java

@ -40,7 +40,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_C
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_TYPE; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_TYPE;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_EXTERNAL_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_NAME; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_TENANT_ID_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_TENANT_ID_COLUMN;
@ -82,9 +81,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
@Column(name = DEBUG_SETTINGS) @Column(name = DEBUG_SETTINGS)
private String debugSettings; private String debugSettings;
@Column(name = CALCULATED_FIELD_EXTERNAL_ID)
private UUID externalId;
public CalculatedFieldEntity() { public CalculatedFieldEntity() {
super(); super();
} }
@ -101,9 +97,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration()); this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration());
this.version = calculatedField.getVersion(); this.version = calculatedField.getVersion();
this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings()); this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings());
if (calculatedField.getExternalId() != null) {
this.externalId = calculatedField.getExternalId().getId();
}
} }
@Override @Override
@ -118,9 +111,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class)); calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class));
calculatedField.setVersion(version); calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
if (externalId != null) {
calculatedField.setExternalId(new CalculatedFieldId(externalId));
}
return calculatedField; return calculatedField;
} }

1
dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java

@ -92,7 +92,6 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class)); calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class));
calculatedField.setVersion(version); calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
calculatedField.setExternalId(externalIdObj != null ? new CalculatedFieldId(UUID.fromString((String) externalIdObj)) : null);
return calculatedField; return calculatedField;
}).collect(Collectors.toList()); }).collect(Collectors.toList());

4
dao/src/main/resources/sql/schema-entities.sql

@ -936,9 +936,7 @@ CREATE TABLE IF NOT EXISTS calculated_field (
configuration varchar(1000000), configuration varchar(1000000),
version BIGINT DEFAULT 1, version BIGINT DEFAULT 1,
debug_settings varchar(1024), debug_settings varchar(1024),
external_id UUID, CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name)
CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name),
CONSTRAINT calculated_field_external_id_unq_key UNIQUE (tenant_id, external_id)
); );
CREATE TABLE IF NOT EXISTS calculated_field_link ( CREATE TABLE IF NOT EXISTS calculated_field_link (

14
dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java

@ -101,20 +101,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
.hasMessage("Calculated Field with such name is already in exists!"); .hasMessage("Calculated Field with such name is already in exists!");
} }
@Test
public void testSaveCalculatedFieldWithExistingExternalId() {
Device device = createTestDevice();
CalculatedField calculatedField = getCalculatedField(device.getId(), device.getId());
calculatedField.setExternalId(new CalculatedFieldId(UUID.fromString("2ef69d0a-89cf-4868-86f8-c50551d87ebe")));
calculatedFieldService.save(calculatedField);
calculatedField.setName("Test 2");
assertThatThrownBy(() -> calculatedFieldService.save(calculatedField))
.isInstanceOf(DataValidationException.class)
.hasMessage("Calculated Field with such external id already exists!");
}
@Test @Test
public void testFindCalculatedFieldById() { public void testFindCalculatedFieldById() {
CalculatedField savedCalculatedField = saveValidCalculatedField(); CalculatedField savedCalculatedField = saveValidCalculatedField();

3
ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html

@ -84,6 +84,9 @@
<mat-checkbox formControlName="saveRelations"> <mat-checkbox formControlName="saveRelations">
{{ 'version-control.export-relations' | translate }} {{ 'version-control.export-relations' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
</div> </div>
</div> </div>
</div> </div>

14
ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts

@ -25,7 +25,11 @@ import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { catchError, map, mergeMap } from 'rxjs/operators'; import { catchError, map, mergeMap } from 'rxjs/operators';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { EntityTypeVersionCreateConfig, exportableEntityTypes } from '@shared/models/vc.models'; import {
EntityTypeVersionCreateConfig,
exportableEntityTypes,
typesWithCalculatedFields
} from '@shared/models/vc.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@ -43,6 +47,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit
isReadOnly: Observable<boolean>; isReadOnly: Observable<boolean>;
readonly typesWithCalculatedFields = typesWithCalculatedFields;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private adminService: AdminService, private adminService: AdminService,
private dialogService: DialogService, private dialogService: DialogService,
@ -104,7 +110,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit
branch: null, branch: null,
saveAttributes: true, saveAttributes: true,
saveRelations: false, saveRelations: false,
saveCredentials: true saveCredentials: true,
saveCalculatedFields: true,
}; };
const allowed = this.allowedEntityTypes(); const allowed = this.allowedEntityTypes();
let entityType: EntityType = null; let entityType: EntityType = null;
@ -206,7 +213,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit
branch: [config.branch, []], branch: [config.branch, []],
saveRelations: [config.saveRelations, []], saveRelations: [config.saveRelations, []],
saveAttributes: [config.saveAttributes, []], saveAttributes: [config.saveAttributes, []],
saveCredentials: [config.saveCredentials, []] saveCredentials: [config.saveCredentials, []],
saveCalculatedFields: [config.saveCalculatedFields, []]
}) })
} }
); );

3
ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html

@ -72,6 +72,9 @@
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations"> <mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations">
{{ 'version-control.export-relations' | translate }} {{ 'version-control.export-relations' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
</div> </div>
</div> </div>
</div> </div>

7
ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts

@ -33,7 +33,8 @@ import {
EntityTypeVersionCreateConfig, EntityTypeVersionCreateConfig,
exportableEntityTypes, exportableEntityTypes,
SyncStrategy, SyncStrategy,
syncStrategyTranslationMap syncStrategyTranslationMap,
typesWithCalculatedFields
} from '@shared/models/vc.models'; } from '@shared/models/vc.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -79,6 +80,8 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
loading = true; loading = true;
readonly typesWithCalculatedFields = typesWithCalculatedFields;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private translate: TranslateService, private translate: TranslateService,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
@ -150,6 +153,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
saveRelations: [config.saveRelations, []], saveRelations: [config.saveRelations, []],
saveAttributes: [config.saveAttributes, []], saveAttributes: [config.saveAttributes, []],
saveCredentials: [config.saveCredentials, []], saveCredentials: [config.saveCredentials, []],
saveCalculatedFields: [config.saveCalculatedFields, []],
allEntities: [config.allEntities, []], allEntities: [config.allEntities, []],
entityIds: [config.entityIds, [Validators.required]] entityIds: [config.entityIds, [Validators.required]]
}) })
@ -202,6 +206,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
saveAttributes: true, saveAttributes: true,
saveRelations: true, saveRelations: true,
saveCredentials: true, saveCredentials: true,
saveCalculatedFields: true,
allEntities: true, allEntities: true,
entityIds: [] entityIds: []
}; };

3
ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html

@ -72,6 +72,9 @@
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadRelations"> <mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadRelations">
{{ 'version-control.load-relations' | translate }} {{ 'version-control.load-relations' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadCalculatedFields">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
</div> </div>
</div> </div>
</div> </div>

7
ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts

@ -31,7 +31,8 @@ import { PageComponent } from '@shared/components/page.component';
import { import {
entityTypesWithoutRelatedData, entityTypesWithoutRelatedData,
EntityTypeVersionLoadConfig, EntityTypeVersionLoadConfig,
exportableEntityTypes exportableEntityTypes,
typesWithCalculatedFields
} from '@shared/models/vc.models'; } from '@shared/models/vc.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -75,6 +76,8 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
loading = true; loading = true;
readonly typesWithCalculatedFields = typesWithCalculatedFields;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private translate: TranslateService, private translate: TranslateService,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
@ -145,6 +148,7 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
loadRelations: [config.loadRelations, []], loadRelations: [config.loadRelations, []],
loadAttributes: [config.loadAttributes, []], loadAttributes: [config.loadAttributes, []],
loadCredentials: [config.loadCredentials, []], loadCredentials: [config.loadCredentials, []],
loadCalculatedFields: [config.loadCalculatedFields, []],
removeOtherEntities: [config.removeOtherEntities, []], removeOtherEntities: [config.removeOtherEntities, []],
findExistingEntityByName: [config.findExistingEntityByName, []] findExistingEntityByName: [config.findExistingEntityByName, []]
}) })
@ -180,6 +184,7 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
loadAttributes: true, loadAttributes: true,
loadRelations: true, loadRelations: true,
loadCredentials: true, loadCredentials: true,
loadCalculatedFields: true,
removeOtherEntities: false, removeOtherEntities: false,
findExistingEntityByName: true findExistingEntityByName: true
}; };

3
ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html

@ -47,6 +47,9 @@
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveRelations" style="margin-bottom: 16px;"> <mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveRelations" style="margin-bottom: 16px;">
{{ 'version-control.export-relations' | translate }} {{ 'version-control.export-relations' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityId.entityType)" formControlName="saveCalculatedFields" class="mb-4">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
</div> </div>
</fieldset> </fieldset>
</form> </form>

6
ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts

@ -20,6 +20,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { import {
entityTypesWithoutRelatedData, entityTypesWithoutRelatedData,
SingleEntityVersionCreateRequest, SingleEntityVersionCreateRequest,
typesWithCalculatedFields,
VersionCreateRequestType, VersionCreateRequestType,
VersionCreationResult VersionCreationResult
} from '@shared/models/vc.models'; } from '@shared/models/vc.models';
@ -71,6 +72,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni
private versionCreateResultSubscription: Subscription; private versionCreateResultSubscription: Subscription;
readonly typesWithCalculatedFields = typesWithCalculatedFields;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService, private entitiesVersionControlService: EntitiesVersionControlService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
@ -115,7 +118,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni
? this.createVersionFormGroup.get('saveRelations').value : false, ? this.createVersionFormGroup.get('saveRelations').value : false,
saveAttributes: !entityTypesWithoutRelatedData.has(this.entityId.entityType) saveAttributes: !entityTypesWithoutRelatedData.has(this.entityId.entityType)
? this.createVersionFormGroup.get('saveAttributes').value : false, ? this.createVersionFormGroup.get('saveAttributes').value : false,
saveCredentials: this.entityId.entityType === EntityType.DEVICE ? this.createVersionFormGroup.get('saveCredentials').value : false saveCredentials: this.entityId.entityType === EntityType.DEVICE ? this.createVersionFormGroup.get('saveCredentials').value : false,
saveCalculatedFields: typesWithCalculatedFields.has(this.entityId.entityType) ? this.createVersionFormGroup.get('saveCalculatedFields').value : false,
}, },
type: VersionCreateRequestType.SINGLE_ENTITY type: VersionCreateRequestType.SINGLE_ENTITY
}; };

3
ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html

@ -36,6 +36,9 @@
<mat-checkbox *ngIf="entityDataInfo.hasRelations" formControlName="loadRelations" style="margin-bottom: 16px;"> <mat-checkbox *ngIf="entityDataInfo.hasRelations" formControlName="loadRelations" style="margin-bottom: 16px;">
{{ 'version-control.load-relations' | translate }} {{ 'version-control.load-relations' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox *ngIf="entityDataInfo.hasCalculatedFields" formControlName="loadCalculatedFields" class="mb-4">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
</div> </div>
</fieldset> </fieldset>
</form> </form>

6
ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts

@ -79,7 +79,8 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn
this.restoreFormGroup = this.fb.group({ this.restoreFormGroup = this.fb.group({
loadAttributes: [true, []], loadAttributes: [true, []],
loadRelations: [true, []], loadRelations: [true, []],
loadCredentials: [true, []] loadCredentials: [true, []],
loadCalculatedFields: [true, []]
}); });
this.entitiesVersionControlService.getEntityDataInfo(this.externalEntityId, this.versionId).subscribe((data) => { this.entitiesVersionControlService.getEntityDataInfo(this.externalEntityId, this.versionId).subscribe((data) => {
this.entityDataInfo = data; this.entityDataInfo = data;
@ -110,7 +111,8 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn
config: { config: {
loadRelations: this.entityDataInfo.hasRelations ? this.restoreFormGroup.get('loadRelations').value : false, loadRelations: this.entityDataInfo.hasRelations ? this.restoreFormGroup.get('loadRelations').value : false,
loadAttributes: this.entityDataInfo.hasAttributes ? this.restoreFormGroup.get('loadAttributes').value : false, loadAttributes: this.entityDataInfo.hasAttributes ? this.restoreFormGroup.get('loadAttributes').value : false,
loadCredentials: this.entityDataInfo.hasCredentials ? this.restoreFormGroup.get('loadCredentials').value : false loadCredentials: this.entityDataInfo.hasCredentials ? this.restoreFormGroup.get('loadCredentials').value : false,
loadCalculatedFields: this.entityDataInfo.hasCalculatedFields ? this.restoreFormGroup.get('loadCalculatedFields').value : false
}, },
type: VersionLoadRequestType.SINGLE_ENTITY type: VersionLoadRequestType.SINGLE_ENTITY
}; };

7
ui-ngx/src/app/shared/models/vc.models.ts

@ -49,6 +49,7 @@ export interface VersionCreateConfig {
saveRelations: boolean; saveRelations: boolean;
saveAttributes: boolean; saveAttributes: boolean;
saveCredentials: boolean; saveCredentials: boolean;
saveCalculatedFields: boolean;
} }
export enum VersionCreateRequestType { export enum VersionCreateRequestType {
@ -106,6 +107,7 @@ export function createDefaultEntityTypesVersionCreate(): {[entityType: string]:
syncStrategy: null, syncStrategy: null,
saveAttributes: !entityTypesWithoutRelatedData.has(entityType), saveAttributes: !entityTypesWithoutRelatedData.has(entityType),
saveRelations: !entityTypesWithoutRelatedData.has(entityType), saveRelations: !entityTypesWithoutRelatedData.has(entityType),
saveCalculatedFields: typesWithCalculatedFields.has(entityType),
saveCredentials: true, saveCredentials: true,
allEntities: true, allEntities: true,
entityIds: [] entityIds: []
@ -118,6 +120,7 @@ export interface VersionLoadConfig {
loadRelations: boolean; loadRelations: boolean;
loadAttributes: boolean; loadAttributes: boolean;
loadCredentials: boolean; loadCredentials: boolean;
loadCalculatedFields: boolean;
} }
export enum VersionLoadRequestType { export enum VersionLoadRequestType {
@ -154,6 +157,7 @@ export function createDefaultEntityTypesVersionLoad(): {[entityType: string]: En
loadAttributes: !entityTypesWithoutRelatedData.has(entityType), loadAttributes: !entityTypesWithoutRelatedData.has(entityType),
loadRelations: !entityTypesWithoutRelatedData.has(entityType), loadRelations: !entityTypesWithoutRelatedData.has(entityType),
loadCredentials: true, loadCredentials: true,
loadCalculatedFields: typesWithCalculatedFields.has(entityType),
removeOtherEntities: false, removeOtherEntities: false,
findExistingEntityByName: true findExistingEntityByName: true
}; };
@ -254,4 +258,7 @@ export interface EntityDataInfo {
hasRelations: boolean; hasRelations: boolean;
hasAttributes: boolean; hasAttributes: boolean;
hasCredentials: boolean; hasCredentials: boolean;
hasCalculatedFields: boolean;
} }
export const typesWithCalculatedFields = new Set<EntityType | AliasEntityType>([EntityType.DEVICE, EntityType.ASSET, EntityType.ASSET_PROFILE, EntityType.DEVICE_PROFILE]);

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

@ -6320,6 +6320,7 @@
"export-relations": "Export relations", "export-relations": "Export relations",
"export-attributes": "Export attributes", "export-attributes": "Export attributes",
"export-credentials": "Export credentials", "export-credentials": "Export credentials",
"export-calculated-fields": "Export calculated fields",
"entity-versions": "Entity versions", "entity-versions": "Entity versions",
"versions": "Versions", "versions": "Versions",
"created-time": "Created time", "created-time": "Created time",
@ -6336,6 +6337,7 @@
"load-relations": "Load relations", "load-relations": "Load relations",
"load-attributes": "Load attributes", "load-attributes": "Load attributes",
"load-credentials": "Load credentials", "load-credentials": "Load credentials",
"load-calculated-fields": "Load calculated fields",
"compare-with-current": "Compare with current", "compare-with-current": "Compare with current",
"diff-entity-with-version": "Diff with entity version '{{versionName}}'", "diff-entity-with-version": "Diff with entity version '{{versionName}}'",
"previous-difference": "Previous Difference", "previous-difference": "Previous Difference",

Loading…
Cancel
Save