diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index cf46d0f7fd..c036f70e4a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -1503,11 +1503,12 @@ public class DeviceControllerTest extends AbstractControllerTest { Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty()); Device updatedDevice = doGet("/api/device/" + savedDevice.getId().getId(), Device.class); + savedDevice.setVersion(updatedDevice.getVersion()); Assert.assertEquals(savedDevice, updatedDevice); DeviceCredentials updatedCredentials = doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); - + savedCredentials.setVersion(updatedCredentials.getVersion()); Assert.assertEquals(savedCredentials, updatedCredentials); } diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index 949dd4a781..4edc3de01e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -30,6 +30,8 @@ import org.springframework.test.context.ContextConfiguration; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbCreateAlarmNode; import org.thingsboard.rule.engine.action.TbCreateAlarmNodeConfiguration; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration; import org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode; import org.thingsboard.rule.engine.metadata.TbGetRelatedDataNodeConfiguration; import org.thingsboard.server.common.data.StringUtils; @@ -356,6 +358,47 @@ public class RuleChainControllerTest extends AbstractControllerTest { assertThat(error).contains("alarmType is malformed"); } + @Test + public void testSaveRuleChainWithOutdatedVersion() throws Exception { + RuleChain ruleChain = createRuleChain("My rule chain"); + + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + RuleNode ruleNode = new RuleNode(); + ruleNode.setName("Test"); + ruleNode.setType(TbMsgGeneratorNode.class.getName()); + TbMsgGeneratorNodeConfiguration config = new TbMsgGeneratorNodeConfiguration(); + ruleNode.setConfiguration(JacksonUtil.valueToTree(config)); + List ruleNodes = new ArrayList<>(); + ruleNodes.add(ruleNode); + ruleChainMetaData.setFirstNodeIndex(0); + ruleChainMetaData.setNodes(ruleNodes); + + ruleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + assertThat(ruleChainMetaData.getVersion()).isEqualTo(2); + + ruleChain = doGet("/api/ruleChain/" + ruleChain.getId(), RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(2); + + ruleChain.setName("Updated"); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(3); + + ruleChain.setVersion(1L); + doPost("/api/ruleChain", ruleChain) + .andExpect(status().isConflict()); + ruleChainMetaData.setVersion(1L); + doPost("/api/ruleChain/metadata", ruleChainMetaData) + .andExpect(status().isConflict()); + + ruleChainMetaData.setVersion(3L); + ruleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + assertThat(ruleChainMetaData.getVersion()).isEqualTo(4); + ruleChain.setVersion(4L); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(5); + } + private RuleChain createRuleChain(String name) { RuleChain ruleChain = new RuleChain(); ruleChain.setName(name); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java index 310c6bccf7..e6ed0b824a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java @@ -15,10 +15,16 @@ */ package org.thingsboard.server.common.data.exception; +import org.thingsboard.server.common.data.EntityType; + public class EntityVersionMismatchException extends RuntimeException { public EntityVersionMismatchException(String message, Throwable cause) { super(message, cause); } + public EntityVersionMismatchException(EntityType entityType, Throwable cause) { + this((entityType != null ? entityType.getNormalName() : "Entity") + " was already changed by someone else", cause); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java index eb18e28c8c..0ccbd1d34a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.rule; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.id.RuleChainId; import java.util.ArrayList; @@ -27,11 +28,14 @@ import java.util.List; */ @Schema @Data -public class RuleChainMetaData { +public class RuleChainMetaData implements HasVersion { @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Rule Chain Id.", accessMode = Schema.AccessMode.READ_ONLY) private RuleChainId ruleChainId; + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Version of the Rule Chain") + private Long version; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of the first rule node in the 'nodes' list") private Integer firstNodeIndex; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java index 98eaff96c9..cccffd6fed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; public class RuleChainExportData extends EntityExportData { @JsonProperty(index = 3) - @JsonIgnoreProperties("ruleChainId") + @JsonIgnoreProperties({"ruleChainId", "version"}) private RuleChainMetaData metaData; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index fea3980446..902bf0f6a7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -167,13 +168,15 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return saveRuleChainMetaData(tenantId, ruleChainMetaData, ruleNodeUpdater, true); } - + @Transactional @Override public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { return RuleChainUpdateResult.failed(); + } else if (ruleChainMetaData.getVersion() != null && !ruleChainMetaData.getVersion().equals(ruleChain.getVersion())) { + throw new EntityVersionMismatchException(EntityType.RULE_CHAIN, null); } RuleChainDataValidator.validateMetaDataFieldsAndConnections(ruleChainMetaData); @@ -234,7 +237,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId)) || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { ruleChain.setFirstRuleNodeId(firstRuleNodeId); - ruleChainDao.save(tenantId, ruleChain); } if (ruleChainMetaData.getConnections() != null) { for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) { @@ -283,6 +285,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (!relations.isEmpty()) { relationService.saveRelations(tenantId, relations); } + ruleChain = ruleChainDao.save(tenantId, ruleChain); if (publishSaveEvent) { eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain).entityId(ruleChain.getId()).build()); } @@ -298,6 +301,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); ruleChainMetaData.setRuleChainId(ruleChainId); + ruleChainMetaData.setVersion(ruleChain.getVersion()); List ruleNodes = getRuleChainNodes(tenantId, ruleChainId); Collections.sort(ruleNodes, Comparator.comparingLong(RuleNode::getCreatedTime).thenComparing(RuleNode::getId, Comparator.comparing(RuleNodeId::getId))); Map ruleNodeIndexMap = new HashMap<>(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 801898386e..f044629364 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -79,7 +79,7 @@ public abstract class JpaAbstractDao, D> try { entity = doSave(entity, isNew, flush); } catch (OptimisticLockException e) { - throw new EntityVersionMismatchException((getEntityType() != null ? getEntityType().getNormalName() : "Entity") + " was already changed by someone else", e); + throw new EntityVersionMismatchException(getEntityType(), e); } return DaoUtil.getData(entity); } @@ -97,20 +97,28 @@ public abstract class JpaAbstractDao, D> if (versionedEntity.getVersion() == null) { HasVersion existingEntity = entityManager.find(versionedEntity.getClass(), entity.getUuid()); if (existingEntity != null) { - versionedEntity.setVersion(existingEntity.getVersion()); // manually resetting the version to latest to allow force overwrite of the entity + /* + * manually resetting the version to latest to allow force overwrite of the entity + * */ + versionedEntity.setVersion(existingEntity.getVersion()); } else { return doSave(entity, true, flush); } } - entity = entityManager.merge(entity); + versionedEntity = entityManager.merge(versionedEntity); + /* + * by default, Hibernate doesn't issue an update query and thus version increment + * if the entity was not modified. to bypass this and always increment the version, we do it manually + * */ + versionedEntity.setVersion(versionedEntity.getVersion() + 1); /* - * flushing so that the query is executed right away and the version is incremented, - * then removing the entity from the persistence context so that it is not affected + * flushing and then removing the entity from the persistence context so that it is not affected * by next flushes (e.g. when a transaction is committed) to avoid double version increment * */ entityManager.flush(); - entityManager.detach(entity); + entityManager.detach(versionedEntity); flushed = true; + entity = (E) versionedEntity; } else { entity = entityManager.merge(entity); } @@ -161,6 +169,7 @@ public abstract class JpaAbstractDao, D> log.debug("Remove request: {}", id); } + @Override @Transactional public void removeAllByIds(Collection ids) { JpaRepository repository = getRepository(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java index 644ce40bb8..90510cf2b7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java @@ -45,6 +45,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -105,7 +106,37 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { @Test public void testSaveDeviceName0x00_thenSomeDatabaseException() { Device device = getDevice(tenantId1, customerId1, "\u0000"); - assertThatThrownBy(() -> deviceIds.add(deviceDao.save(TenantId.fromUUID(tenantId1), device).getUuidId())); + assertThatThrownBy(() -> deviceIds.add(saveDevice(tenantId1, device).getUuidId())); + } + + @Test + public void testSaveDevice_versionIncrement() { + Device device = getDevice(tenantId1, customerId1, "1ewfewf2"); + device = saveDevice(tenantId1, device); + deviceIds.add(device.getUuidId()); + assertThat(device.getVersion()).isEqualTo(1); + + device.setName(device.getName() + "x"); + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(2); + + device.setName(device.getName() + "x"); + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(3); + } + + @Test + public void testSaveDevice_versionIncrement_noChanges() { + Device device = getDevice(tenantId1, customerId1, "1ewfewf2"); + device = saveDevice(tenantId1, device); + deviceIds.add(device.getUuidId()); + assertThat(device.getVersion()).isEqualTo(1); + + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(2); + + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(3); } @Test @@ -126,7 +157,7 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { UUID customerId = Uuids.timeBased(); // send to method getDevice() number = 40, because make random name is bad and name "SEARCH_TEXT_40" don't used Device device = getDevice(tenantId, customerId, 40); - deviceIds.add(deviceDao.save(TenantId.fromUUID(tenantId), device).getUuidId()); + deviceIds.add(saveDevice(tenantId, device).getUuidId()); UUID uuid = device.getId().getId(); Device entity = deviceDao.findById(TenantId.fromUUID(tenantId), uuid); @@ -156,8 +187,8 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { private List createDevices(UUID tenantId1, UUID tenantId2, UUID customerId1, UUID customerId2, int count) { List savedDevicesUUID = new ArrayList<>(); for (int i = 0; i < count / 2; i++) { - savedDevicesUUID.add(deviceDao.save(TenantId.fromUUID(tenantId1), getDevice(tenantId1, customerId1, i)).getUuidId()); - savedDevicesUUID.add(deviceDao.save(TenantId.fromUUID(tenantId2), getDevice(tenantId2, customerId2, i + count / 2)).getUuidId()); + savedDevicesUUID.add(saveDevice(tenantId1, getDevice(tenantId1, customerId1, i)).getUuidId()); + savedDevicesUUID.add(saveDevice(tenantId2, getDevice(tenantId2, customerId2, i + count / 2)).getUuidId()); } return savedDevicesUUID; } @@ -175,4 +206,9 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { device.setDeviceProfileId(savedDeviceProfile.getId()); return device; } + + private Device saveDevice(UUID tenantId, Device device) { + return deviceDao.save(TenantId.fromUUID(tenantId), device); + } + } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 46c84087ee..eb640a3af8 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -77,8 +77,8 @@ import { } from '@shared/models/rule-node.models'; import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models'; import { RuleChainService } from '@core/http/rule-chain.service'; -import { NEVER, Observable, of, ReplaySubject, skip, startWith, Subject } from 'rxjs'; -import { debounceTime, distinctUntilChanged, mergeMap, takeUntil, tap } from 'rxjs/operators'; +import { NEVER, Observable, of, ReplaySubject, skip, startWith, Subject, throwError } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { ISearchableComponent } from '../../models/searchable-component.models'; import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; @@ -93,6 +93,7 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { ComponentClusteringMode } from '@shared/models/component-descriptor.models'; import { MatDrawer } from '@angular/material/sidenav'; +import { HttpStatusCode } from '@angular/common/http'; import Timeout = NodeJS.Timeout; @Component({ @@ -1282,7 +1283,9 @@ export class RuleChainPageComponent extends PageComponent onDebugEventSelected(debugEventBody: DebugRuleNodeEventBody) { const ruleNodeConfigComponent = this.ruleNodeComponent.ruleNodeConfigComponent; const ruleNodeConfigDefinedComponent = ruleNodeConfigComponent.definedConfigComponent; - if (ruleNodeConfigComponent.useDefinedDirective() && ruleNodeConfigDefinedComponent.hasScript && ruleNodeConfigDefinedComponent.testScript) { + if (ruleNodeConfigComponent.useDefinedDirective() + && ruleNodeConfigDefinedComponent.hasScript + && ruleNodeConfigDefinedComponent.testScript) { ruleNodeConfigDefinedComponent.testScript(debugEventBody); } } @@ -1456,7 +1459,8 @@ export class RuleChainPageComponent extends PageComponent const ruleChainMetaData: RuleChainMetaData = { ruleChainId: this.ruleChain.id, nodes: [], - connections: [] + connections: [], + version: ruleChain.version }; const nodes: FcRuleNode[] = []; this.ruleChainModel.nodes.forEach((node) => { @@ -1465,7 +1469,9 @@ export class RuleChainPageComponent extends PageComponent id: node.ruleNodeId, type: node.component.clazz, name: node.name, - configurationVersion: isDefinedAndNotNull(node.configurationVersion) ? node.configurationVersion : node.component.configurationVersion, + configurationVersion: isDefinedAndNotNull(node.configurationVersion) + ? node.configurationVersion + : node.component.configurationVersion, configuration: node.configuration, additionalInfo: node.additionalInfo ? node.additionalInfo : {}, debugMode: node.debugMode, @@ -1500,20 +1506,30 @@ export class RuleChainPageComponent extends PageComponent }); } }); - this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData).subscribe((savedRuleChainMetaData) => { - this.ruleChainMetaData = savedRuleChainMetaData; - if (this.isImport) { - this.isDirtyValue = false; - this.isImport = false; - if (this.ruleChainType !== RuleChainType.EDGE) { - this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); + this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData) + .pipe( + catchError(err => { + if (err.status === HttpStatusCode.Conflict) { + return this.ruleChainService.getRuleChainMetadata(ruleChainMetaData.ruleChainId.id); + } + return throwError(() => err); + }) + ) + .subscribe((savedRuleChainMetaData) => { + this.ruleChain.version = savedRuleChainMetaData.version; + this.ruleChainMetaData = savedRuleChainMetaData; + if (this.isImport) { + this.isDirtyValue = false; + this.isImport = false; + if (this.ruleChainType !== RuleChainType.EDGE) { + this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); + } else { + this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`); + } } else { - this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`); + this.createRuleChainModel(); } - } else { - this.createRuleChainModel(); - } - saveResult.next(); + saveResult.next(); }); }); return saveResult; diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html index ba09311895..2e16f04498 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html @@ -30,7 +30,7 @@
{{ 'entity.version-conflict.link' | translate: - { entityType: (entityTypeTranslations.get(data.entity.id.entityType).type | translate) } + { entityType: (entityTypeTranslations.get(entityId.entityType).type | translate) } }} {{ 'entity.link' | translate }}. diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts index 37275bd79c..b4f34ed232 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts @@ -21,10 +21,12 @@ import { ImportExportService } from '@shared/import-export/import-export.service import { CommonModule } from '@angular/common'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityInfoData } from '@shared/models/entity.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { RuleChainMetaData } from '@shared/models/rule-chain.models'; interface EntityConflictDialogData { message: string; - entity: EntityInfoData; + entity: EntityInfoData | RuleChainMetaData; } @Component({ @@ -38,13 +40,18 @@ interface EntityConflictDialogData { ], }) export class EntityConflictDialogComponent { + + entityId: EntityId; + readonly entityTypeTranslations = entityTypeTranslations; constructor( @Inject(MAT_DIALOG_DATA) public data: EntityConflictDialogData, private dialogRef: MatDialogRef, private importExportService: ImportExportService, - ) {} + ) { + this.entityId = (data.entity as EntityInfoData).id ?? (data.entity as RuleChainMetaData).ruleChainId; + } onCancel(): void { this.dialogRef.close(); diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 4746365c50..5f246873d7 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -55,7 +55,11 @@ import { EntityType } from '@shared/models/entity-type.models'; import { UtilsService } from '@core/services/utils.service'; import { WidgetService } from '@core/http/widget.service'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; -import { EntityInfoData, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models'; +import { + EntityInfoData, + ImportEntitiesResultInfo, + ImportEntityData +} from '@shared/models/entity.models'; import { RequestConfig } from '@core/http/http-utils'; import { RuleChain, RuleChainImport, RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainService } from '@core/http/rule-chain.service'; @@ -360,25 +364,26 @@ export class ImportExportService { }); } - public exportEntity(entityData: EntityInfoData): void { + public exportEntity(entityData: EntityInfoData | RuleChainMetaData): void { + const id = (entityData as EntityInfoData).id ?? (entityData as RuleChainMetaData).ruleChainId; let preparedData; - switch (entityData.id.entityType) { + switch (id.entityType) { case EntityType.DEVICE_PROFILE: case EntityType.ASSET_PROFILE: preparedData = this.prepareProfileExport(entityData as DeviceProfile | AssetProfile); break; case EntityType.RULE_CHAIN: - this.ruleChainService.getRuleChainMetadata(entityData.id.id) + forkJoin([this.ruleChainService.getRuleChainMetadata(id.id), this.ruleChainService.getRuleChain(id.id)]) .pipe( take(1), - map((ruleChainMetaData) => { + map(([ruleChainMetaData, ruleChain]) => { const ruleChainExport: RuleChainImport = { - ruleChain: this.prepareRuleChain(entityData as RuleChain), + ruleChain: this.prepareRuleChain(ruleChain), metadata: this.prepareRuleChainMetaData(ruleChainMetaData) }; return ruleChainExport; })) - .subscribe(ruleChainData => this.exportToPc(ruleChainData, entityData.name)); + .subscribe(this.onRuleChainExported()); return; case EntityType.WIDGETS_BUNDLE: this.exportSelectedWidgetsBundle(entityData as WidgetsBundle); @@ -389,7 +394,7 @@ export class ImportExportService { default: preparedData = this.prepareExport(entityData); } - this.exportToPc(preparedData, entityData.name); + this.exportToPc(preparedData, (entityData as EntityInfoData).name); } private exportSelectedWidgetsBundle(widgetsBundle: WidgetsBundle): void { @@ -584,8 +589,12 @@ export class ImportExportService { return ruleChainExport; }) )) - ).subscribe({ - next: (ruleChainExport) => { + ).subscribe(this.onRuleChainExported()); + } + + private onRuleChainExported() { + return { + next: (ruleChainExport: RuleChainImport) => { let name = ruleChainExport.ruleChain.name; name = name.toLowerCase().replace(/\W/g, '_'); this.exportToPc(ruleChainExport, name); @@ -593,7 +602,7 @@ export class ImportExportService { error: (e) => { this.handleExportError(e, 'rulechain.export-failed-error'); } - }); + }; } public importRuleChain(expectedRuleChainType: RuleChainType): Observable { diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts index 0bfdfbc459..bca4ae399e 100644 --- a/ui-ngx/src/app/shared/models/rule-chain.models.ts +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -34,7 +34,7 @@ export interface RuleChain extends BaseData, HasTenantId, HasVersio isDefault?: boolean; } -export interface RuleChainMetaData { +export interface RuleChainMetaData extends HasVersion { ruleChainId: RuleChainId; firstNodeIndex?: number; nodes: Array;