Browse Source

Merge pull request #11496 from thingsboard/feature/rule-chain-versioning

Versioning for rule chain metadata
pull/11569/head
Viacheslav Klimov 2 years ago
committed by GitHub
parent
commit
d88558eee0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  2. 43
      application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
  3. 6
      common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java
  4. 6
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
  5. 2
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java
  6. 8
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  7. 21
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
  8. 44
      dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java
  9. 50
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  10. 2
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html
  11. 11
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts
  12. 31
      ui-ngx/src/app/shared/import-export/import-export.service.ts
  13. 2
      ui-ngx/src/app/shared/models/rule-chain.models.ts

3
application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java

@ -1503,11 +1503,12 @@ public class DeviceControllerTest extends AbstractControllerTest {
Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty()); Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty());
Device updatedDevice = doGet("/api/device/" + savedDevice.getId().getId(), Device.class); Device updatedDevice = doGet("/api/device/" + savedDevice.getId().getId(), Device.class);
savedDevice.setVersion(updatedDevice.getVersion());
Assert.assertEquals(savedDevice, updatedDevice); Assert.assertEquals(savedDevice, updatedDevice);
DeviceCredentials updatedCredentials = DeviceCredentials updatedCredentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
savedCredentials.setVersion(updatedCredentials.getVersion());
Assert.assertEquals(savedCredentials, updatedCredentials); Assert.assertEquals(savedCredentials, updatedCredentials);
} }

43
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.common.util.JacksonUtil;
import org.thingsboard.rule.engine.action.TbCreateAlarmNode; import org.thingsboard.rule.engine.action.TbCreateAlarmNode;
import org.thingsboard.rule.engine.action.TbCreateAlarmNodeConfiguration; 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.TbGetRelatedAttributeNode;
import org.thingsboard.rule.engine.metadata.TbGetRelatedDataNodeConfiguration; import org.thingsboard.rule.engine.metadata.TbGetRelatedDataNodeConfiguration;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -356,6 +358,47 @@ public class RuleChainControllerTest extends AbstractControllerTest {
assertThat(error).contains("alarmType is malformed"); 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<RuleNode> 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) { private RuleChain createRuleChain(String name) {
RuleChain ruleChain = new RuleChain(); RuleChain ruleChain = new RuleChain();
ruleChain.setName(name); ruleChain.setName(name);

6
common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java

@ -15,10 +15,16 @@
*/ */
package org.thingsboard.server.common.data.exception; package org.thingsboard.server.common.data.exception;
import org.thingsboard.server.common.data.EntityType;
public class EntityVersionMismatchException extends RuntimeException { public class EntityVersionMismatchException extends RuntimeException {
public EntityVersionMismatchException(String message, Throwable cause) { public EntityVersionMismatchException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
public EntityVersionMismatchException(EntityType entityType, Throwable cause) {
this((entityType != null ? entityType.getNormalName() : "Entity") + " was already changed by someone else", cause);
}
} }

6
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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import java.util.ArrayList; import java.util.ArrayList;
@ -27,11 +28,14 @@ import java.util.List;
*/ */
@Schema @Schema
@Data @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) @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Rule Chain Id.", accessMode = Schema.AccessMode.READ_ONLY)
private RuleChainId ruleChainId; 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") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of the first rule node in the 'nodes' list")
private Integer firstNodeIndex; private Integer firstNodeIndex;

2
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<RuleChain> { public class RuleChainExportData extends EntityExportData<RuleChain> {
@JsonProperty(index = 3) @JsonProperty(index = 3)
@JsonIgnoreProperties("ruleChainId") @JsonIgnoreProperties({"ruleChainId", "version"})
private RuleChainMetaData metaData; private RuleChainMetaData metaData;
} }

8
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.EntityType;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge; 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.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId; 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); return saveRuleChainMetaData(tenantId, ruleChainMetaData, ruleNodeUpdater, true);
} }
@Transactional
@Override @Override
public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function<RuleNode, RuleNode> ruleNodeUpdater, boolean publishSaveEvent) { public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function<RuleNode, RuleNode> ruleNodeUpdater, boolean publishSaveEvent) {
Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id.");
RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId());
if (ruleChain == null) { if (ruleChain == null) {
return RuleChainUpdateResult.failed(); return RuleChainUpdateResult.failed();
} else if (ruleChainMetaData.getVersion() != null && !ruleChainMetaData.getVersion().equals(ruleChain.getVersion())) {
throw new EntityVersionMismatchException(EntityType.RULE_CHAIN, null);
} }
RuleChainDataValidator.validateMetaDataFieldsAndConnections(ruleChainMetaData); RuleChainDataValidator.validateMetaDataFieldsAndConnections(ruleChainMetaData);
@ -234,7 +237,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId)) if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId))
|| (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) {
ruleChain.setFirstRuleNodeId(firstRuleNodeId); ruleChain.setFirstRuleNodeId(firstRuleNodeId);
ruleChainDao.save(tenantId, ruleChain);
} }
if (ruleChainMetaData.getConnections() != null) { if (ruleChainMetaData.getConnections() != null) {
for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) { for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) {
@ -283,6 +285,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
if (!relations.isEmpty()) { if (!relations.isEmpty()) {
relationService.saveRelations(tenantId, relations); relationService.saveRelations(tenantId, relations);
} }
ruleChain = ruleChainDao.save(tenantId, ruleChain);
if (publishSaveEvent) { if (publishSaveEvent) {
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain).entityId(ruleChain.getId()).build()); 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 ruleChainMetaData = new RuleChainMetaData();
ruleChainMetaData.setRuleChainId(ruleChainId); ruleChainMetaData.setRuleChainId(ruleChainId);
ruleChainMetaData.setVersion(ruleChain.getVersion());
List<RuleNode> ruleNodes = getRuleChainNodes(tenantId, ruleChainId); List<RuleNode> ruleNodes = getRuleChainNodes(tenantId, ruleChainId);
Collections.sort(ruleNodes, Comparator.comparingLong(RuleNode::getCreatedTime).thenComparing(RuleNode::getId, Comparator.comparing(RuleNodeId::getId))); Collections.sort(ruleNodes, Comparator.comparingLong(RuleNode::getCreatedTime).thenComparing(RuleNode::getId, Comparator.comparing(RuleNodeId::getId)));
Map<RuleNodeId, Integer> ruleNodeIndexMap = new HashMap<>(); Map<RuleNodeId, Integer> ruleNodeIndexMap = new HashMap<>();

21
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java

@ -79,7 +79,7 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
try { try {
entity = doSave(entity, isNew, flush); entity = doSave(entity, isNew, flush);
} catch (OptimisticLockException e) { } 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); return DaoUtil.getData(entity);
} }
@ -97,20 +97,28 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
if (versionedEntity.getVersion() == null) { if (versionedEntity.getVersion() == null) {
HasVersion existingEntity = entityManager.find(versionedEntity.getClass(), entity.getUuid()); HasVersion existingEntity = entityManager.find(versionedEntity.getClass(), entity.getUuid());
if (existingEntity != null) { 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 { } else {
return doSave(entity, true, flush); 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, * flushing and then removing the entity from the persistence context so that it is not affected
* 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 * by next flushes (e.g. when a transaction is committed) to avoid double version increment
* */ * */
entityManager.flush(); entityManager.flush();
entityManager.detach(entity); entityManager.detach(versionedEntity);
flushed = true; flushed = true;
entity = (E) versionedEntity;
} else { } else {
entity = entityManager.merge(entity); entity = entityManager.merge(entity);
} }
@ -161,6 +169,7 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
log.debug("Remove request: {}", id); log.debug("Remove request: {}", id);
} }
@Override
@Transactional @Transactional
public void removeAllByIds(Collection<UUID> ids) { public void removeAllByIds(Collection<UUID> ids) {
JpaRepository<E, UUID> repository = getRepository(); JpaRepository<E, UUID> repository = getRepository();

44
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.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -105,7 +106,37 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest {
@Test @Test
public void testSaveDeviceName0x00_thenSomeDatabaseException() { public void testSaveDeviceName0x00_thenSomeDatabaseException() {
Device device = getDevice(tenantId1, customerId1, "\u0000"); 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 @Test
@ -126,7 +157,7 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest {
UUID customerId = Uuids.timeBased(); UUID customerId = Uuids.timeBased();
// send to method getDevice() number = 40, because make random name is bad and name "SEARCH_TEXT_40" don't used // 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); 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(); UUID uuid = device.getId().getId();
Device entity = deviceDao.findById(TenantId.fromUUID(tenantId), uuid); Device entity = deviceDao.findById(TenantId.fromUUID(tenantId), uuid);
@ -156,8 +187,8 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest {
private List<UUID> createDevices(UUID tenantId1, UUID tenantId2, UUID customerId1, UUID customerId2, int count) { private List<UUID> createDevices(UUID tenantId1, UUID tenantId2, UUID customerId1, UUID customerId2, int count) {
List<UUID> savedDevicesUUID = new ArrayList<>(); List<UUID> savedDevicesUUID = new ArrayList<>();
for (int i = 0; i < count / 2; i++) { for (int i = 0; i < count / 2; i++) {
savedDevicesUUID.add(deviceDao.save(TenantId.fromUUID(tenantId1), getDevice(tenantId1, customerId1, i)).getUuidId()); savedDevicesUUID.add(saveDevice(tenantId1, getDevice(tenantId1, customerId1, i)).getUuidId());
savedDevicesUUID.add(deviceDao.save(TenantId.fromUUID(tenantId2), getDevice(tenantId2, customerId2, i + count / 2)).getUuidId()); savedDevicesUUID.add(saveDevice(tenantId2, getDevice(tenantId2, customerId2, i + count / 2)).getUuidId());
} }
return savedDevicesUUID; return savedDevicesUUID;
} }
@ -175,4 +206,9 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest {
device.setDeviceProfileId(savedDeviceProfile.getId()); device.setDeviceProfileId(savedDeviceProfile.getId());
return device; return device;
} }
private Device saveDevice(UUID tenantId, Device device) {
return deviceDao.save(TenantId.fromUUID(tenantId), device);
}
} }

50
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -77,8 +77,8 @@ import {
} from '@shared/models/rule-node.models'; } from '@shared/models/rule-node.models';
import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models'; import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models';
import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainService } from '@core/http/rule-chain.service';
import { NEVER, Observable, of, ReplaySubject, skip, startWith, Subject } from 'rxjs'; import { NEVER, Observable, of, ReplaySubject, skip, startWith, Subject, throwError } from 'rxjs';
import { debounceTime, distinctUntilChanged, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { catchError, debounceTime, distinctUntilChanged, mergeMap, takeUntil, tap } from 'rxjs/operators';
import { ISearchableComponent } from '../../models/searchable-component.models'; import { ISearchableComponent } from '../../models/searchable-component.models';
import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; 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 { VersionControlComponent } from '@home/components/vc/version-control.component';
import { ComponentClusteringMode } from '@shared/models/component-descriptor.models'; import { ComponentClusteringMode } from '@shared/models/component-descriptor.models';
import { MatDrawer } from '@angular/material/sidenav'; import { MatDrawer } from '@angular/material/sidenav';
import { HttpStatusCode } from '@angular/common/http';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
@Component({ @Component({
@ -1282,7 +1283,9 @@ export class RuleChainPageComponent extends PageComponent
onDebugEventSelected(debugEventBody: DebugRuleNodeEventBody) { onDebugEventSelected(debugEventBody: DebugRuleNodeEventBody) {
const ruleNodeConfigComponent = this.ruleNodeComponent.ruleNodeConfigComponent; const ruleNodeConfigComponent = this.ruleNodeComponent.ruleNodeConfigComponent;
const ruleNodeConfigDefinedComponent = ruleNodeConfigComponent.definedConfigComponent; const ruleNodeConfigDefinedComponent = ruleNodeConfigComponent.definedConfigComponent;
if (ruleNodeConfigComponent.useDefinedDirective() && ruleNodeConfigDefinedComponent.hasScript && ruleNodeConfigDefinedComponent.testScript) { if (ruleNodeConfigComponent.useDefinedDirective()
&& ruleNodeConfigDefinedComponent.hasScript
&& ruleNodeConfigDefinedComponent.testScript) {
ruleNodeConfigDefinedComponent.testScript(debugEventBody); ruleNodeConfigDefinedComponent.testScript(debugEventBody);
} }
} }
@ -1456,7 +1459,8 @@ export class RuleChainPageComponent extends PageComponent
const ruleChainMetaData: RuleChainMetaData = { const ruleChainMetaData: RuleChainMetaData = {
ruleChainId: this.ruleChain.id, ruleChainId: this.ruleChain.id,
nodes: [], nodes: [],
connections: [] connections: [],
version: ruleChain.version
}; };
const nodes: FcRuleNode[] = []; const nodes: FcRuleNode[] = [];
this.ruleChainModel.nodes.forEach((node) => { this.ruleChainModel.nodes.forEach((node) => {
@ -1465,7 +1469,9 @@ export class RuleChainPageComponent extends PageComponent
id: node.ruleNodeId, id: node.ruleNodeId,
type: node.component.clazz, type: node.component.clazz,
name: node.name, name: node.name,
configurationVersion: isDefinedAndNotNull(node.configurationVersion) ? node.configurationVersion : node.component.configurationVersion, configurationVersion: isDefinedAndNotNull(node.configurationVersion)
? node.configurationVersion
: node.component.configurationVersion,
configuration: node.configuration, configuration: node.configuration,
additionalInfo: node.additionalInfo ? node.additionalInfo : {}, additionalInfo: node.additionalInfo ? node.additionalInfo : {},
debugMode: node.debugMode, debugMode: node.debugMode,
@ -1500,20 +1506,30 @@ export class RuleChainPageComponent extends PageComponent
}); });
} }
}); });
this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData).subscribe((savedRuleChainMetaData) => { this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData)
this.ruleChainMetaData = savedRuleChainMetaData; .pipe(
if (this.isImport) { catchError(err => {
this.isDirtyValue = false; if (err.status === HttpStatusCode.Conflict) {
this.isImport = false; return this.ruleChainService.getRuleChainMetadata(ruleChainMetaData.ruleChainId.id);
if (this.ruleChainType !== RuleChainType.EDGE) { }
this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.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 { } else {
this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`); this.createRuleChainModel();
} }
} else { saveResult.next();
this.createRuleChainModel();
}
saveResult.next();
}); });
}); });
return saveResult; return saveResult;

2
ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html

@ -30,7 +30,7 @@
<div class="message-container"> <div class="message-container">
<span> <span>
{{ 'entity.version-conflict.link' | translate: {{ 'entity.version-conflict.link' | translate:
{ entityType: (entityTypeTranslations.get(data.entity.id.entityType).type | translate) } { entityType: (entityTypeTranslations.get(entityId.entityType).type | translate) }
}} }}
<a class="cursor-pointer" (click)="onLinkClick($event)">{{ 'entity.link' | translate }}</a>. <a class="cursor-pointer" (click)="onLinkClick($event)">{{ 'entity.link' | translate }}</a>.
</span> </span>

11
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 { CommonModule } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityInfoData } from '@shared/models/entity.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 { interface EntityConflictDialogData {
message: string; message: string;
entity: EntityInfoData; entity: EntityInfoData | RuleChainMetaData;
} }
@Component({ @Component({
@ -38,13 +40,18 @@ interface EntityConflictDialogData {
], ],
}) })
export class EntityConflictDialogComponent { export class EntityConflictDialogComponent {
entityId: EntityId;
readonly entityTypeTranslations = entityTypeTranslations; readonly entityTypeTranslations = entityTypeTranslations;
constructor( constructor(
@Inject(MAT_DIALOG_DATA) public data: EntityConflictDialogData, @Inject(MAT_DIALOG_DATA) public data: EntityConflictDialogData,
private dialogRef: MatDialogRef<EntityConflictDialogComponent>, private dialogRef: MatDialogRef<EntityConflictDialogComponent>,
private importExportService: ImportExportService, private importExportService: ImportExportService,
) {} ) {
this.entityId = (data.entity as EntityInfoData).id ?? (data.entity as RuleChainMetaData).ruleChainId;
}
onCancel(): void { onCancel(): void {
this.dialogRef.close(); this.dialogRef.close();

31
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 { UtilsService } from '@core/services/utils.service';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; 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 { RequestConfig } from '@core/http/http-utils';
import { RuleChain, RuleChainImport, RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChain, RuleChainImport, RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service'; 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; let preparedData;
switch (entityData.id.entityType) { switch (id.entityType) {
case EntityType.DEVICE_PROFILE: case EntityType.DEVICE_PROFILE:
case EntityType.ASSET_PROFILE: case EntityType.ASSET_PROFILE:
preparedData = this.prepareProfileExport(entityData as DeviceProfile | AssetProfile); preparedData = this.prepareProfileExport(entityData as DeviceProfile | AssetProfile);
break; break;
case EntityType.RULE_CHAIN: case EntityType.RULE_CHAIN:
this.ruleChainService.getRuleChainMetadata(entityData.id.id) forkJoin([this.ruleChainService.getRuleChainMetadata(id.id), this.ruleChainService.getRuleChain(id.id)])
.pipe( .pipe(
take(1), take(1),
map((ruleChainMetaData) => { map(([ruleChainMetaData, ruleChain]) => {
const ruleChainExport: RuleChainImport = { const ruleChainExport: RuleChainImport = {
ruleChain: this.prepareRuleChain(entityData as RuleChain), ruleChain: this.prepareRuleChain(ruleChain),
metadata: this.prepareRuleChainMetaData(ruleChainMetaData) metadata: this.prepareRuleChainMetaData(ruleChainMetaData)
}; };
return ruleChainExport; return ruleChainExport;
})) }))
.subscribe(ruleChainData => this.exportToPc(ruleChainData, entityData.name)); .subscribe(this.onRuleChainExported());
return; return;
case EntityType.WIDGETS_BUNDLE: case EntityType.WIDGETS_BUNDLE:
this.exportSelectedWidgetsBundle(entityData as WidgetsBundle); this.exportSelectedWidgetsBundle(entityData as WidgetsBundle);
@ -389,7 +394,7 @@ export class ImportExportService {
default: default:
preparedData = this.prepareExport(entityData); preparedData = this.prepareExport(entityData);
} }
this.exportToPc(preparedData, entityData.name); this.exportToPc(preparedData, (entityData as EntityInfoData).name);
} }
private exportSelectedWidgetsBundle(widgetsBundle: WidgetsBundle): void { private exportSelectedWidgetsBundle(widgetsBundle: WidgetsBundle): void {
@ -584,8 +589,12 @@ export class ImportExportService {
return ruleChainExport; return ruleChainExport;
}) })
)) ))
).subscribe({ ).subscribe(this.onRuleChainExported());
next: (ruleChainExport) => { }
private onRuleChainExported() {
return {
next: (ruleChainExport: RuleChainImport) => {
let name = ruleChainExport.ruleChain.name; let name = ruleChainExport.ruleChain.name;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(ruleChainExport, name); this.exportToPc(ruleChainExport, name);
@ -593,7 +602,7 @@ export class ImportExportService {
error: (e) => { error: (e) => {
this.handleExportError(e, 'rulechain.export-failed-error'); this.handleExportError(e, 'rulechain.export-failed-error');
} }
}); };
} }
public importRuleChain(expectedRuleChainType: RuleChainType): Observable<RuleChainImport> { public importRuleChain(expectedRuleChainType: RuleChainType): Observable<RuleChainImport> {

2
ui-ngx/src/app/shared/models/rule-chain.models.ts

@ -34,7 +34,7 @@ export interface RuleChain extends BaseData<RuleChainId>, HasTenantId, HasVersio
isDefault?: boolean; isDefault?: boolean;
} }
export interface RuleChainMetaData { export interface RuleChainMetaData extends HasVersion {
ruleChainId: RuleChainId; ruleChainId: RuleChainId;
firstNodeIndex?: number; firstNodeIndex?: number;
nodes: Array<RuleNode>; nodes: Array<RuleNode>;

Loading…
Cancel
Save