diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 178dde33e2..06fe7f4036 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/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.EntityImportResult; 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.relation.RelationService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -61,6 +62,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS private final Map> importServices = new HashMap<>(); private final RelationService relationService; + private final CalculatedFieldService calculatedFieldService; private final RateLimitService rateLimitService; private final TbLogEntityActionService logEntityActionService; @@ -72,7 +74,6 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ); - @Override public , I extends EntityId> EntityExportData exportEntity(EntitiesExportCtx ctx, I entityId) throws ThingsboardException { if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) { @@ -129,13 +130,11 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS } } - @Override public Comparator getEntityTypeComparatorForImport() { return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf); } - @SuppressWarnings("unchecked") private , D extends EntityExportData> EntityExportService getExportService(EntityType entityType) { EntityExportService exportService = exportServices.get(entityType); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java index 2b7e7d3593..b77447df8d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java @@ -32,7 +32,6 @@ public interface EntitiesExportImportService { , I extends EntityId> EntityImportResult importEntity(EntitiesImportCtx ctx, EntityExportData exportData) throws ThingsboardException; - void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException; Comparator getEntityTypeComparatorForImport(); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index b0bea68290..5c2fcc7dc6 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/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.ExportableEntity; 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.id.EntityId; 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.EntityExportData; 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.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.ie.exporting.EntityExportService; @@ -59,6 +61,8 @@ public class DefaultEntityExportService ctx, I entityId) throws ThingsboardException { @@ -98,6 +102,10 @@ public class DefaultEntityExportService> attributes = exportAttributes(ctx, entity); exportData.setAttributes(attributes); } + if (ctx.getSettings().isExportCalculatedFields()) { + List calculatedFields = exportCalculatedFields(ctx, entity.getId()); + exportData.setCalculatedFields(calculatedFields); + } } private List exportRelations(EntitiesExportCtx ctx, E entity) throws ThingsboardException { @@ -141,6 +149,19 @@ public class DefaultEntityExportService exportCalculatedFields(EntitiesExportCtx ctx, EntityId entityId) { + List 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 getExternalIdOrElseInternal(EntitiesExportCtx ctx, ID internalId) { if (internalId == null || internalId.isNullUid()) return internalId; var result = ctx.getExternalId(internalId); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java index 00ce23b2e8..7cd4c3aca1 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java @@ -47,7 +47,11 @@ public class AssetImportService extends BaseEntityImportService 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 diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java index 97c54957bf..32a0090a4a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java @@ -50,7 +50,11 @@ public class AssetProfileImportService extends BaseEntityImportService 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 diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 7044b235a0..bfa95af83c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/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.User; 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.id.EntityId; 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.EntityExportData; 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.RelationService; import org.thingsboard.server.service.action.EntityActionService; @@ -78,6 +80,8 @@ public abstract class BaseEntityImportService existing = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), savedEntity.getId()); + List 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 { logEntityActionService.logEntityAction(user.getTenantId(), savedEntity.getId(), savedEntity, null, oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, user); } - @SuppressWarnings("unchecked") protected E findExistingEntity(EntitiesImportCtx ctx, E entity, IdProvider idProvider) { return (E) Optional.ofNullable(entitiesService.findEntityByTenantIdAndExternalId(ctx.getTenantId(), entity.getId())) @@ -313,10 +364,10 @@ public abstract class BaseEntityImportService new MissingEntityException(externalId)); } - @SuppressWarnings("unchecked") @RequiredArgsConstructor protected class IdProvider { + private final EntitiesImportCtx ctx; private final EntityImportResult importResult; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java index 61b9839cbd..84e264efdd 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java @@ -64,13 +64,18 @@ public class DeviceImportService extends BaseEntityImportService 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 diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index eed18abe52..89b51ce84a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/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.concurrent.ExecutionException; import java.util.function.Function; -import java.util.stream.Collectors; import static com.google.common.util.concurrent.Futures.transform; import static org.thingsboard.server.common.data.sync.vc.VcUtils.checkBranchName; @@ -304,6 +303,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont .updateRelations(config.isLoadRelations()) .saveAttributes(config.isLoadAttributes()) .saveCredentials(config.isLoadCredentials()) + .saveCalculatedFields(config.isLoadCalculatedFields()) .findExistingByName(false) .build()); ctx.setFinalImportAttempt(true); @@ -327,7 +327,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont var sw = TbStopWatch.create("before"); List entityTypes = request.getEntityTypes().keySet().stream() - .sorted(exportImportService.getEntityTypeComparatorForImport()).collect(Collectors.toList()); + .sorted(exportImportService.getEntityTypeComparatorForImport()).toList(); for (EntityType entityType : entityTypes) { log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType); sw.startNew("Entities " + entityType.name()); @@ -362,6 +362,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont .updateRelations(config.isLoadRelations()) .saveAttributes(config.isLoadAttributes()) .saveCredentials(config.isLoadCredentials()) + .saveCalculatedFields(config.isLoadCalculatedFields()) .findExistingByName(config.isFindExistingEntityByName()) .build(); } @@ -471,7 +472,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } @Override - public ListenableFuture compareEntityDataToVersion(User user, EntityId entityId, String versionId) throws Exception { + public ListenableFuture compareEntityDataToVersion(User user, EntityId entityId, String versionId) { HasId entity = exportableEntitiesService.findEntityByTenantIdAndId(user.getTenantId(), entityId); if (!(entity instanceof ExportableEntity)) throw new IllegalArgumentException("Unsupported entity type"); @@ -484,6 +485,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont .exportRelations(otherVersion.hasRelations()) .exportAttributes(otherVersion.hasAttributes()) .exportCredentials(otherVersion.hasCredentials()) + .exportCalculatedFields(otherVersion.hasCalculatedFields()) .build()); EntityExportData currentVersion; try { @@ -503,7 +505,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override - public ListenableFuture> listBranches(TenantId tenantId) throws Exception { + public ListenableFuture> listBranches(TenantId tenantId) { return gitServiceQueue.listBranches(tenantId); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java index 669dad347a..b3007c6738 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java @@ -69,6 +69,7 @@ public abstract class EntitiesExportCtx { .exportRelations(config.isSaveRelations()) .exportAttributes(config.isSaveAttributes()) .exportCredentials(config.isSaveCredentials()) + .exportCalculatedFields(config.isSaveCalculatedFields()) .build(); } @@ -85,4 +86,5 @@ public abstract class EntitiesExportCtx { log.debug("[{}][{}] Local cache put: {}", internalId.getEntityType(), internalId.getId(), externalId); externalIdMap.put(internalId, externalId != null ? externalId : internalId); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java index ab836fa13d..e24683cd5a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java @@ -91,6 +91,10 @@ public class EntitiesImportCtx { return getSettings().isSaveCredentials(); } + public boolean isSaveCalculatedFields() { + return getSettings().isSaveCalculatedFields(); + } + public EntityId getInternalId(EntityId externalId) { var result = externalToInternalIdMap.get(externalId); 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); } - } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java index e0a5b40281..55c8a763b0 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java @@ -39,6 +39,7 @@ public class EntityTypeExportCtx extends EntitiesExportCtx .exportRelations(config.isSaveRelations()) .exportAttributes(config.isSaveAttributes()) .exportCredentials(config.isSaveCredentials()) + .exportCalculatedFields(config.isSaveCalculatedFields()) .build(); this.overwrite = ObjectUtils.defaultIfNull(config.getSyncStrategy(), defaultSyncStrategy) == SyncStrategy.OVERWRITE; } diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 7b4d6923f0..da3b214afb 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/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.AssetProfile; 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.DeviceData; 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.RuleNode; 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.EntityExportSettings; 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.dao.asset.AssetProfileService; 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.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; @@ -145,6 +156,8 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { protected TenantService tenantService; @Autowired protected EntityViewService entityViewService; + @Autowired + protected CalculatedFieldService calculatedFieldService; protected TenantId tenantId1; protected User tenantAdmin1; @@ -191,6 +204,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); + CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId()); Map entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) @@ -198,6 +212,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { try { return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() .exportCredentials(false) + .exportCalculatedFields(true) .build()); } catch (Exception 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), any(), eq(ActionType.UPDATED), isNull()); - EntityExportData updatedAssetEntity = getAndClone(entitiesExportData, EntityType.ASSET); updatedAssetEntity.getEntity().setLabel("t" + updatedAssetEntity.getEntity().getLabel()); Asset updatedAsset = importEntity(tenantAdmin2, updatedAssetEntity).getSavedEntity(); @@ -268,10 +282,27 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)); verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice)); + // calculated field of imported device: + List 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 updatedDeviceEntity = getAndClone(entitiesExportData, EntityType.DEVICE); updatedDeviceEntity.getEntity().setLabel("t" + updatedDeviceEntity.getEntity().getLabel()); Device updatedDevice = importEntity(tenantAdmin2, updatedDeviceEntity).getSavedEntity(); 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 @@ -290,12 +321,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1"); EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1"); + CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId()); + Map ids = new HashMap<>(); 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())) { EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId); EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder() .saveCredentials(false) + .saveCalculatedFields(true) .build()); ids.put(entityId, (EntityId) importResult.getSavedEntity().getId()); } @@ -325,10 +359,16 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId()); assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId()); - Device exportedDevice = (Device) exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())).getEntity(); + EntityExportData entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())); + Device exportedDevice = entityExportData.getEntity(); assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId()); + List 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(); assertThat(exportedEntityView.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedEntityView.getEntityId()).isEqualTo(device.getId()); @@ -340,7 +380,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { deviceProfileService.saveDeviceProfile(importedDeviceProfile); } - protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) { Device device = new Device(); device.setTenantId(tenantId); @@ -549,9 +588,43 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { 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 , I extends EntityId> EntityExportData exportEntity(User user, I entityId) throws Exception { return exportEntity(user, entityId, EntityExportSettings.builder() .exportCredentials(true) + .exportCalculatedFields(true) .build()); } @@ -562,6 +635,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { protected , I extends EntityId> EntityImportResult importEntity(User user, EntityExportData exportData) throws Exception { return importEntity(user, exportData, EntityImportSettings.builder() .saveCredentials(true) + .saveCalculatedFields(true) .build()); } diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 0088ae2b7b..06f61ca4e5 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/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.asset.Asset; 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.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; @@ -562,6 +571,81 @@ public class VersionControlTest extends AbstractControllerTest { 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 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 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 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 & HasTenantId> void checkImportedEntity(TenantId tenantId1, E initialEntity, TenantId tenantId2, E importedEntity) { assertThat(initialEntity.getTenantId()).isEqualTo(tenantId1); 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 -> { EntityTypeVersionCreateConfig config = new EntityTypeVersionCreateConfig(); config.setAllEntities(true); - config.setSaveRelations(true); config.setSaveAttributes(true); config.setSaveCredentials(true); + config.setSaveCalculatedFields(true); return config; }))); @@ -695,6 +779,7 @@ public class VersionControlTest extends AbstractControllerTest { config.setLoadAttributes(true); config.setLoadRelations(true); config.setLoadCredentials(true); + config.setLoadCalculatedFields(true); config.setRemoveOtherEntities(false); config.setFindExistingEntityByName(true); configModifier.accept(config); @@ -941,6 +1026,38 @@ public class VersionControlTest extends AbstractControllerTest { 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) { assertThat(importedRuleChain.getType()).isEqualTo(initialRuleChain.getType()); assertThat(importedRuleChain.getName()).isEqualTo(initialRuleChain.getName()); @@ -995,11 +1112,18 @@ public class VersionControlTest extends AbstractControllerTest { private RuleChain findRuleChain(String name) throws Exception { return doGetTypedWithPageLink("/api/ruleChains?", new TypeReference>() {}, new PageLink(100, 0, name)).getData().get(0); - } private RuleChainMetaData findRuleChainMetaData(RuleChainId ruleChainId) throws Exception { 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>() {}, new PageLink(100, 0)).getData().get(0); + } + + private List findCalculatedFieldsByEntityId(EntityId entityId) throws Exception { + return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields?", new TypeReference>() {}, new PageLink(100, 0)).getData(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java index d4aa0b60a3..b86f30ca78 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java +++ b/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.Setter; 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.HasName; 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.NoXss; +import java.io.Serial; + @Schema @Data @EqualsAndHashCode(callSuper = true) -public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, ExportableEntity, HasDebugSettings { +public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings { + @Serial private static final long serialVersionUID = 4491966747773381420L; private TenantId tenantId; @@ -66,9 +68,6 @@ public class CalculatedField extends BaseData implements HasN @Getter @Setter private Long version; - @Getter - @Setter - private CalculatedFieldId externalId; public CalculatedField() { super(); @@ -78,7 +77,7 @@ public class CalculatedField extends BaseData implements HasN 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.entityId = entityId; this.type = type; @@ -86,7 +85,19 @@ public class CalculatedField extends BaseData implements HasN this.configurationVersion = configurationVersion; this.configuration = configuration; 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.") @@ -112,7 +123,6 @@ public class CalculatedField extends BaseData implements HasN .append(", configurationVersion=").append(configurationVersion) .append(", configuration=").append(configuration) .append(", version=").append(version) - .append(", externalId=").append(externalId) .append(", createdTime=").append(createdTime) .append(", id=").append(id).append(']') .toString(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java index 0a83f1a19f..e17a066d88 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java +++ b/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 org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; @Schema public class CalculatedFieldId extends UUIDBased implements EntityId { + @Serial private static final long serialVersionUID = 1L; @JsonCreator @@ -41,4 +43,5 @@ public class CalculatedFieldId extends UUIDBased implements EntityId { public EntityType getEntityType() { return EntityType.CALCULATED_FIELD; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java index bfa82caa2b..98948ca642 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java @@ -38,4 +38,5 @@ public class DeviceExportData extends EntityExportData { public boolean hasCredentials() { return credentials != null; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java index 7bc9a63c29..072be6acf2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java +++ b/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 org.thingsboard.server.common.data.EntityType; 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.relation.EntityRelation; import org.thingsboard.server.common.data.sync.JsonTbEntity; @@ -55,6 +56,8 @@ public class EntityExportData> { public static final Comparator attrComparator = Comparator .comparing(AttributeExportData::getKey).thenComparing(AttributeExportData::getLastUpdateTs); + public static final Comparator calculatedFieldsComparator = Comparator.comparing(CalculatedField::getName); + @JsonProperty(index = 2) @JsonTbEntity private E entity; @@ -65,6 +68,9 @@ public class EntityExportData> { private List relations; @JsonProperty(index = 101) private Map> attributes; + @JsonProperty(index = 102) + @JsonIgnoreProperties({"id", "entityId", "createdTime", "version"}) + private List calculatedFields; public EntityExportData sort() { if (relations != null && !relations.isEmpty()) { @@ -73,6 +79,9 @@ public class EntityExportData> { if (attributes != null && !attributes.isEmpty()) { attributes.values().forEach(list -> list.sort(attrComparator)); } + if (calculatedFields != null && !calculatedFields.isEmpty()) { + calculatedFields.sort(calculatedFieldsComparator); + } return this; } @@ -96,4 +105,9 @@ public class EntityExportData> { return relations != null; } + @JsonIgnore + public boolean hasCalculatedFields() { + return calculatedFields != null; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java index 46dc6860ee..078ca29e28 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java @@ -25,7 +25,10 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @Builder public class EntityExportSettings { + private boolean exportRelations; private boolean exportAttributes; private boolean exportCredentials; + private boolean exportCalculatedFields; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java index 2abdb55b65..d49801537c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java @@ -25,8 +25,11 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @Builder public class EntityImportSettings { + private boolean findExistingByName; private boolean updateRelations; private boolean saveAttributes; private boolean saveCredentials; + private boolean saveCalculatedFields; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java index 5a02b18364..6d1ac2283b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java +++ b/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 java.io.Serial; import java.io.Serializable; @Data public class VersionCreateConfig implements Serializable { + + @Serial private static final long serialVersionUID = 1223723167716612772L; private boolean saveRelations; private boolean saveAttributes; private boolean saveCredentials; + private boolean saveCalculatedFields; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java index 0ae263dada..7f3ce89372 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java +++ b/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 loadAttributes; private boolean loadCredentials; + private boolean loadCalculatedFields; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java index 90b82a1e1e..13fd6159fc 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java +++ b/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.cluster.ToAllNodesMsg; +import java.io.Serial; import java.util.Optional; /** @@ -33,6 +34,8 @@ import java.util.Optional; */ @Data public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { + + @Serial private static final long serialVersionUID = -5303421482781273062L; private final TenantId tenantId; @@ -66,4 +69,5 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { public MsgType getMsgType() { return MsgType.COMPONENT_LIFE_CYCLE_MSG; } + } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index a570dbea89..f1da00d943 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -135,7 +135,7 @@ public class ProtoUtils { builder.setName(msg.getName()); } if (msg.getOldName() != null) { - builder.setName(msg.getOldName()); + builder.setOldName(msg.getOldName()); } return builder.build(); } @@ -171,7 +171,6 @@ public class ProtoUtils { return entityTypeByProtoNumber[entityType.getNumber()]; } - public static TransportProtos.ToEdgeSyncRequestMsgProto toProto(ToEdgeSyncRequest request) { return TransportProtos.ToEdgeSyncRequestMsgProto.newBuilder() .setTenantIdMSB(request.getTenantId().getId().getMostSignificantBits()) diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 416bff6ea3..dceef73aba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.cf; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index e2b21ed59b..148908d063 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/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 = "configuration"; public static final String CALCULATED_FIELD_VERSION = "version"; - public static final String CALCULATED_FIELD_EXTERNAL_ID = "external_id"; /** * Calculated field links constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java index 349091a1ae..de6a1365b1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java +++ b/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_ENTITY_ID; 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_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_TENANT_ID_COLUMN; @@ -82,9 +81,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity @Column(name = DEBUG_SETTINGS) private String debugSettings; - @Column(name = CALCULATED_FIELD_EXTERNAL_ID) - private UUID externalId; - public CalculatedFieldEntity() { super(); } @@ -101,9 +97,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration()); this.version = calculatedField.getVersion(); this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings()); - if (calculatedField.getExternalId() != null) { - this.externalId = calculatedField.getExternalId().getId(); - } } @Override @@ -118,9 +111,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class)); calculatedField.setVersion(version); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); - if (externalId != null) { - calculatedField.setExternalId(new CalculatedFieldId(externalId)); - } return calculatedField; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java index fae3468f1e..e59ff3f4e6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java +++ b/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.setVersion(version); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); - calculatedField.setExternalId(externalIdObj != null ? new CalculatedFieldId(UUID.fromString((String) externalIdObj)) : null); return calculatedField; }).collect(Collectors.toList()); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 5bd34c4f1d..d158912690 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -936,9 +936,7 @@ CREATE TABLE IF NOT EXISTS calculated_field ( configuration varchar(1000000), version BIGINT DEFAULT 1, debug_settings varchar(1024), - external_id UUID, - CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name), - CONSTRAINT calculated_field_external_id_unq_key UNIQUE (tenant_id, external_id) + CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name) ); CREATE TABLE IF NOT EXISTS calculated_field_link ( diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 5a783355d7..2985aa7620 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/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!"); } - @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 public void testFindCalculatedFieldById() { CalculatedField savedCalculatedField = saveValidCalculatedField(); diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html index 053d0f5006..d80892664d 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html @@ -84,6 +84,9 @@ {{ 'version-control.export-relations' | translate }} + + {{ 'version-control.export-calculated-fields' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts index e0a88a3e09..d55515a959 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts +++ b/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 { catchError, map, mergeMap } from 'rxjs/operators'; 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 { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @@ -43,6 +47,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit isReadOnly: Observable; + readonly typesWithCalculatedFields = typesWithCalculatedFields; + constructor(protected store: Store, private adminService: AdminService, private dialogService: DialogService, @@ -104,7 +110,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit branch: null, saveAttributes: true, saveRelations: false, - saveCredentials: true + saveCredentials: true, + saveCalculatedFields: true, }; const allowed = this.allowedEntityTypes(); let entityType: EntityType = null; @@ -206,7 +213,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit branch: [config.branch, []], saveRelations: [config.saveRelations, []], saveAttributes: [config.saveAttributes, []], - saveCredentials: [config.saveCredentials, []] + saveCredentials: [config.saveCredentials, []], + saveCalculatedFields: [config.saveCalculatedFields, []] }) } ); diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html index b9a9a6c01c..8fdf22f6cd 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html @@ -72,6 +72,9 @@ {{ 'version-control.export-relations' | translate }} + + {{ 'version-control.export-calculated-fields' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts index 8b16905587..8618e405b0 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts @@ -33,7 +33,8 @@ import { EntityTypeVersionCreateConfig, exportableEntityTypes, SyncStrategy, - syncStrategyTranslationMap + syncStrategyTranslationMap, + typesWithCalculatedFields } from '@shared/models/vc.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -79,6 +80,8 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements loading = true; + readonly typesWithCalculatedFields = typesWithCalculatedFields; + constructor(protected store: Store, private translate: TranslateService, private fb: UntypedFormBuilder, @@ -150,6 +153,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements saveRelations: [config.saveRelations, []], saveAttributes: [config.saveAttributes, []], saveCredentials: [config.saveCredentials, []], + saveCalculatedFields: [config.saveCalculatedFields, []], allEntities: [config.allEntities, []], entityIds: [config.entityIds, [Validators.required]] }) @@ -202,6 +206,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements saveAttributes: true, saveRelations: true, saveCredentials: true, + saveCalculatedFields: true, allEntities: true, entityIds: [] }; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html index 4736bb1ee2..b877d19b63 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html @@ -72,6 +72,9 @@ {{ 'version-control.load-relations' | translate }} + + {{ 'version-control.load-calculated-fields' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts index 23f09ae977..f06abaf3a8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts +++ b/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 { entityTypesWithoutRelatedData, EntityTypeVersionLoadConfig, - exportableEntityTypes + exportableEntityTypes, + typesWithCalculatedFields } from '@shared/models/vc.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -75,6 +76,8 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On loading = true; + readonly typesWithCalculatedFields = typesWithCalculatedFields; + constructor(protected store: Store, private translate: TranslateService, private popoverService: TbPopoverService, @@ -145,6 +148,7 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On loadRelations: [config.loadRelations, []], loadAttributes: [config.loadAttributes, []], loadCredentials: [config.loadCredentials, []], + loadCalculatedFields: [config.loadCalculatedFields, []], removeOtherEntities: [config.removeOtherEntities, []], findExistingEntityByName: [config.findExistingEntityByName, []] }) @@ -180,6 +184,7 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On loadAttributes: true, loadRelations: true, loadCredentials: true, + loadCalculatedFields: true, removeOtherEntities: false, findExistingEntityByName: true }; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html index 10c2dc5e40..da4c31a1b8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html @@ -47,6 +47,9 @@ {{ 'version-control.export-relations' | translate }} + + {{ 'version-control.export-calculated-fields' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts index 48b2a2a174..0c4b696ad1 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts +++ b/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 { entityTypesWithoutRelatedData, SingleEntityVersionCreateRequest, + typesWithCalculatedFields, VersionCreateRequestType, VersionCreationResult } from '@shared/models/vc.models'; @@ -71,6 +72,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni private versionCreateResultSubscription: Subscription; + readonly typesWithCalculatedFields = typesWithCalculatedFields; + constructor(protected store: Store, private entitiesVersionControlService: EntitiesVersionControlService, private cd: ChangeDetectorRef, @@ -115,7 +118,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni ? this.createVersionFormGroup.get('saveRelations').value : false, saveAttributes: !entityTypesWithoutRelatedData.has(this.entityId.entityType) ? 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 }; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html index 4bc61757c4..1183223850 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html @@ -36,6 +36,9 @@ {{ 'version-control.load-relations' | translate }} + + {{ 'version-control.load-calculated-fields' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts index df2e8c19ff..4456eb7ce7 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts +++ b/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({ loadAttributes: [true, []], loadRelations: [true, []], - loadCredentials: [true, []] + loadCredentials: [true, []], + loadCalculatedFields: [true, []] }); this.entitiesVersionControlService.getEntityDataInfo(this.externalEntityId, this.versionId).subscribe((data) => { this.entityDataInfo = data; @@ -110,7 +111,8 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn config: { loadRelations: this.entityDataInfo.hasRelations ? this.restoreFormGroup.get('loadRelations').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 }; diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 6cc8de02eb..3795518ffc 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -49,6 +49,7 @@ export interface VersionCreateConfig { saveRelations: boolean; saveAttributes: boolean; saveCredentials: boolean; + saveCalculatedFields: boolean; } export enum VersionCreateRequestType { @@ -106,6 +107,7 @@ export function createDefaultEntityTypesVersionCreate(): {[entityType: string]: syncStrategy: null, saveAttributes: !entityTypesWithoutRelatedData.has(entityType), saveRelations: !entityTypesWithoutRelatedData.has(entityType), + saveCalculatedFields: typesWithCalculatedFields.has(entityType), saveCredentials: true, allEntities: true, entityIds: [] @@ -118,6 +120,7 @@ export interface VersionLoadConfig { loadRelations: boolean; loadAttributes: boolean; loadCredentials: boolean; + loadCalculatedFields: boolean; } export enum VersionLoadRequestType { @@ -154,6 +157,7 @@ export function createDefaultEntityTypesVersionLoad(): {[entityType: string]: En loadAttributes: !entityTypesWithoutRelatedData.has(entityType), loadRelations: !entityTypesWithoutRelatedData.has(entityType), loadCredentials: true, + loadCalculatedFields: typesWithCalculatedFields.has(entityType), removeOtherEntities: false, findExistingEntityByName: true }; @@ -254,4 +258,7 @@ export interface EntityDataInfo { hasRelations: boolean; hasAttributes: boolean; hasCredentials: boolean; + hasCalculatedFields: boolean; } + +export const typesWithCalculatedFields = new Set([EntityType.DEVICE, EntityType.ASSET, EntityType.ASSET_PROFILE, EntityType.DEVICE_PROFILE]); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 02c604d8ae..6db2fa9ff1 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6320,6 +6320,7 @@ "export-relations": "Export relations", "export-attributes": "Export attributes", "export-credentials": "Export credentials", + "export-calculated-fields": "Export calculated fields", "entity-versions": "Entity versions", "versions": "Versions", "created-time": "Created time", @@ -6336,6 +6337,7 @@ "load-relations": "Load relations", "load-attributes": "Load attributes", "load-credentials": "Load credentials", + "load-calculated-fields": "Load calculated fields", "compare-with-current": "Compare with current", "diff-entity-with-version": "Diff with entity version '{{versionName}}'", "previous-difference": "Previous Difference",