Commit ac007bea24ef4244190690a173c6ef323cb0258e

Authored by Dima Landiak
Committed by Andrew Shvayka
1 parent a400bdb9

device swap feature init

Showing 21 changed files with 272 additions and 24 deletions
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
  18 +import com.fasterxml.jackson.core.JsonProcessingException;
18 import com.fasterxml.jackson.databind.ObjectMapper; 19 import com.fasterxml.jackson.databind.ObjectMapper;
19 import com.fasterxml.jackson.databind.node.ArrayNode; 20 import com.fasterxml.jackson.databind.node.ArrayNode;
20 import com.fasterxml.jackson.databind.node.ObjectNode; 21 import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -26,7 +27,6 @@ import org.springframework.beans.factory.annotation.Value; @@ -26,7 +27,6 @@ import org.springframework.beans.factory.annotation.Value;
26 import org.springframework.security.core.Authentication; 27 import org.springframework.security.core.Authentication;
27 import org.springframework.security.core.context.SecurityContextHolder; 28 import org.springframework.security.core.context.SecurityContextHolder;
28 import org.springframework.web.bind.annotation.ExceptionHandler; 29 import org.springframework.web.bind.annotation.ExceptionHandler;
29 -import org.thingsboard.server.common.data.BaseData;  
30 import org.thingsboard.server.common.data.Customer; 30 import org.thingsboard.server.common.data.Customer;
31 import org.thingsboard.server.common.data.Dashboard; 31 import org.thingsboard.server.common.data.Dashboard;
32 import org.thingsboard.server.common.data.DashboardInfo; 32 import org.thingsboard.server.common.data.DashboardInfo;
@@ -39,12 +39,12 @@ import org.thingsboard.server.common.data.HasTenantId; @@ -39,12 +39,12 @@ import org.thingsboard.server.common.data.HasTenantId;
39 import org.thingsboard.server.common.data.Tenant; 39 import org.thingsboard.server.common.data.Tenant;
40 import org.thingsboard.server.common.data.User; 40 import org.thingsboard.server.common.data.User;
41 import org.thingsboard.server.common.data.alarm.Alarm; 41 import org.thingsboard.server.common.data.alarm.Alarm;
42 -import org.thingsboard.server.common.data.id.AlarmId;  
43 import org.thingsboard.server.common.data.alarm.AlarmInfo; 42 import org.thingsboard.server.common.data.alarm.AlarmInfo;
44 import org.thingsboard.server.common.data.asset.Asset; 43 import org.thingsboard.server.common.data.asset.Asset;
45 import org.thingsboard.server.common.data.audit.ActionType; 44 import org.thingsboard.server.common.data.audit.ActionType;
46 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; 45 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
47 import org.thingsboard.server.common.data.exception.ThingsboardException; 46 import org.thingsboard.server.common.data.exception.ThingsboardException;
  47 +import org.thingsboard.server.common.data.id.AlarmId;
48 import org.thingsboard.server.common.data.id.AssetId; 48 import org.thingsboard.server.common.data.id.AssetId;
49 import org.thingsboard.server.common.data.id.CustomerId; 49 import org.thingsboard.server.common.data.id.CustomerId;
50 import org.thingsboard.server.common.data.id.DashboardId; 50 import org.thingsboard.server.common.data.id.DashboardId;
@@ -595,6 +595,12 @@ public abstract class BaseController { @@ -595,6 +595,12 @@ public abstract class BaseController {
595 case ALARM_CLEAR: 595 case ALARM_CLEAR:
596 msgType = DataConstants.ALARM_CLEAR; 596 msgType = DataConstants.ALARM_CLEAR;
597 break; 597 break;
  598 + case SWAPPED_FROM_TENANT:
  599 + msgType = DataConstants.ENTITY_SWAPPED_FROM;
  600 + break;
  601 + case SWAPPED_TO_TENANT:
  602 + msgType = DataConstants.ENTITY_SWAPPED_TO;
  603 + break;
598 } 604 }
599 if (!StringUtils.isEmpty(msgType)) { 605 if (!StringUtils.isEmpty(msgType)) {
600 try { 606 try {
@@ -614,6 +620,16 @@ public abstract class BaseController { @@ -614,6 +620,16 @@ public abstract class BaseController {
614 String strCustomerName = extractParameter(String.class, 2, additionalInfo); 620 String strCustomerName = extractParameter(String.class, 2, additionalInfo);
615 metaData.putValue("unassignedCustomerId", strCustomerId); 621 metaData.putValue("unassignedCustomerId", strCustomerId);
616 metaData.putValue("unassignedCustomerName", strCustomerName); 622 metaData.putValue("unassignedCustomerName", strCustomerName);
  623 + } else if (actionType == ActionType.SWAPPED_FROM_TENANT) {
  624 + String strTenantId = extractParameter(String.class, 0, additionalInfo);
  625 + String strTenantName = extractParameter(String.class, 1, additionalInfo);
  626 + metaData.putValue("swappedFromTenantId", strTenantId);
  627 + metaData.putValue("swappedFromTenantName", strTenantName);
  628 + } else if (actionType == ActionType.SWAPPED_TO_TENANT) {
  629 + String strTenantId = extractParameter(String.class, 0, additionalInfo);
  630 + String strTenantName = extractParameter(String.class, 1, additionalInfo);
  631 + metaData.putValue("swappedToTenantId", strTenantId);
  632 + metaData.putValue("swappedToTenantName", strTenantName);
617 } 633 }
618 ObjectNode entityNode; 634 ObjectNode entityNode;
619 if (entity != null) { 635 if (entity != null) {
@@ -677,5 +693,13 @@ public abstract class BaseController { @@ -677,5 +693,13 @@ public abstract class BaseController {
677 return result; 693 return result;
678 } 694 }
679 695
  696 + protected <E extends HasName> String entityToStr(E entity) {
  697 + try {
  698 + return json.writeValueAsString(json.valueToTree(entity));
  699 + } catch (JsonProcessingException e) {
  700 + log.warn("[{}] Failed to convert entity to string!", entity, e);
  701 + }
  702 + return null;
  703 + }
680 704
681 } 705 }
@@ -39,8 +39,10 @@ import org.thingsboard.server.common.data.DataConstants; @@ -39,8 +39,10 @@ import org.thingsboard.server.common.data.DataConstants;
39 import org.thingsboard.server.common.data.Device; 39 import org.thingsboard.server.common.data.Device;
40 import org.thingsboard.server.common.data.EntitySubtype; 40 import org.thingsboard.server.common.data.EntitySubtype;
41 import org.thingsboard.server.common.data.EntityType; 41 import org.thingsboard.server.common.data.EntityType;
  42 +import org.thingsboard.server.common.data.Tenant;
42 import org.thingsboard.server.common.data.audit.ActionType; 43 import org.thingsboard.server.common.data.audit.ActionType;
43 import org.thingsboard.server.common.data.device.DeviceSearchQuery; 44 import org.thingsboard.server.common.data.device.DeviceSearchQuery;
  45 +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
44 import org.thingsboard.server.common.data.exception.ThingsboardException; 46 import org.thingsboard.server.common.data.exception.ThingsboardException;
45 import org.thingsboard.server.common.data.id.CustomerId; 47 import org.thingsboard.server.common.data.id.CustomerId;
46 import org.thingsboard.server.common.data.id.DeviceId; 48 import org.thingsboard.server.common.data.id.DeviceId;
@@ -48,6 +50,9 @@ import org.thingsboard.server.common.data.id.TenantId; @@ -48,6 +50,9 @@ import org.thingsboard.server.common.data.id.TenantId;
48 import org.thingsboard.server.common.data.page.TextPageData; 50 import org.thingsboard.server.common.data.page.TextPageData;
49 import org.thingsboard.server.common.data.page.TextPageLink; 51 import org.thingsboard.server.common.data.page.TextPageLink;
50 import org.thingsboard.server.common.data.security.DeviceCredentials; 52 import org.thingsboard.server.common.data.security.DeviceCredentials;
  53 +import org.thingsboard.server.common.msg.TbMsg;
  54 +import org.thingsboard.server.common.msg.TbMsgDataType;
  55 +import org.thingsboard.server.common.msg.TbMsgMetaData;
51 import org.thingsboard.server.dao.device.claim.ClaimResponse; 56 import org.thingsboard.server.dao.device.claim.ClaimResponse;
52 import org.thingsboard.server.dao.device.claim.ClaimResult; 57 import org.thingsboard.server.dao.device.claim.ClaimResult;
53 import org.thingsboard.server.dao.exception.IncorrectParameterException; 58 import org.thingsboard.server.dao.exception.IncorrectParameterException;
@@ -70,6 +75,7 @@ public class DeviceController extends BaseController { @@ -70,6 +75,7 @@ public class DeviceController extends BaseController {
70 75
71 private static final String DEVICE_ID = "deviceId"; 76 private static final String DEVICE_ID = "deviceId";
72 private static final String DEVICE_NAME = "deviceName"; 77 private static final String DEVICE_NAME = "deviceName";
  78 + private static final String TENANT_ID = "tenantId";
73 79
74 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 80 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
75 @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.GET) 81 @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.GET)
@@ -481,4 +487,54 @@ public class DeviceController extends BaseController { @@ -481,4 +487,54 @@ public class DeviceController extends BaseController {
481 } 487 }
482 return DataConstants.DEFAULT_SECRET_KEY; 488 return DataConstants.DEFAULT_SECRET_KEY;
483 } 489 }
  490 +
  491 + @PreAuthorize("hasAuthority('TENANT_ADMIN')")
  492 + @RequestMapping(value = "/tenant/{tenantId}/device/{deviceId}", method = RequestMethod.POST)
  493 + @ResponseBody
  494 + public Device swapDevice(@PathVariable(TENANT_ID) String strTenantId,
  495 + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
  496 + checkParameter(TENANT_ID, strTenantId);
  497 + checkParameter(DEVICE_ID, strDeviceId);
  498 + try {
  499 + DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
  500 + Device device = checkDeviceId(deviceId, Operation.WRITE);
  501 +
  502 + TenantId newTenantId = new TenantId(toUUID(strTenantId));
  503 + Tenant newTenant = tenantService.findTenantById(newTenantId);
  504 + if (newTenant == null) {
  505 + throw new ThingsboardException("Could not find the specified Tenant!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
  506 + }
  507 +
  508 + Device swappedDevice = deviceService.swapDevice(newTenantId, device);
  509 +
  510 + logEntityAction(getCurrentUser(), deviceId, swappedDevice,
  511 + swappedDevice.getCustomerId(),
  512 + ActionType.SWAPPED_TO_TENANT, null, strTenantId, newTenant.getName());
  513 +
  514 + Tenant currentTenant = tenantService.findTenantById(getTenantId());
  515 + pushSwappedFromNotification(currentTenant, newTenantId, swappedDevice);
  516 +
  517 + return swappedDevice;
  518 + } catch (Exception e) {
  519 + logEntityAction(getCurrentUser(), emptyId(EntityType.DEVICE), null,
  520 + null,
  521 + ActionType.SWAPPED_TO_TENANT, e, strTenantId);
  522 + throw handleException(e);
  523 + }
  524 + }
  525 +
  526 + private void pushSwappedFromNotification(Tenant currentTenant, TenantId newTenantId, Device swappedDevice) {
  527 + String data = entityToStr(swappedDevice);
  528 + if (data != null) {
  529 + TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_SWAPPED_FROM, swappedDevice.getId(), getMetaDataForSwappedFrom(currentTenant), TbMsgDataType.JSON, data);
  530 + tbClusterService.pushMsgToRuleEngine(newTenantId, swappedDevice.getId(), tbMsg, null);
  531 + }
  532 + }
  533 +
  534 + private TbMsgMetaData getMetaDataForSwappedFrom(Tenant tenant) {
  535 + TbMsgMetaData metaData = new TbMsgMetaData();
  536 + metaData.putValue("swappedFromTenantId", tenant.getId().getId().toString());
  537 + metaData.putValue("swappedFromTenantName", tenant.getName());
  538 + return metaData;
  539 + }
484 } 540 }
@@ -16,14 +16,12 @@ @@ -16,14 +16,12 @@
16 package org.thingsboard.server.dao.audit; 16 package org.thingsboard.server.dao.audit;
17 17
18 import com.google.common.util.concurrent.ListenableFuture; 18 import com.google.common.util.concurrent.ListenableFuture;
19 -import org.thingsboard.server.common.data.BaseData;  
20 import org.thingsboard.server.common.data.HasName; 19 import org.thingsboard.server.common.data.HasName;
21 import org.thingsboard.server.common.data.audit.ActionType; 20 import org.thingsboard.server.common.data.audit.ActionType;
22 import org.thingsboard.server.common.data.audit.AuditLog; 21 import org.thingsboard.server.common.data.audit.AuditLog;
23 import org.thingsboard.server.common.data.id.CustomerId; 22 import org.thingsboard.server.common.data.id.CustomerId;
24 import org.thingsboard.server.common.data.id.EntityId; 23 import org.thingsboard.server.common.data.id.EntityId;
25 import org.thingsboard.server.common.data.id.TenantId; 24 import org.thingsboard.server.common.data.id.TenantId;
26 -import org.thingsboard.server.common.data.id.UUIDBased;  
27 import org.thingsboard.server.common.data.id.UserId; 25 import org.thingsboard.server.common.data.id.UserId;
28 import org.thingsboard.server.common.data.page.TimePageData; 26 import org.thingsboard.server.common.data.page.TimePageData;
29 import org.thingsboard.server.common.data.page.TimePageLink; 27 import org.thingsboard.server.common.data.page.TimePageLink;
@@ -50,4 +48,5 @@ public interface AuditLogService { @@ -50,4 +48,5 @@ public interface AuditLogService {
50 ActionType actionType, 48 ActionType actionType,
51 Exception e, Object... additionalInfo); 49 Exception e, Object... additionalInfo);
52 50
  51 + void removeAuditLogs(TenantId tenantId, EntityId entityId);
53 } 52 }
@@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.page.TextPageLink; @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
28 import java.util.List; 28 import java.util.List;
29 29
30 public interface DeviceService { 30 public interface DeviceService {
31 - 31 +
32 Device findDeviceById(TenantId tenantId, DeviceId deviceId); 32 Device findDeviceById(TenantId tenantId, DeviceId deviceId);
33 33
34 ListenableFuture<Device> findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId); 34 ListenableFuture<Device> findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId);
@@ -65,4 +65,6 @@ public interface DeviceService { @@ -65,4 +65,6 @@ public interface DeviceService {
65 65
66 ListenableFuture<List<EntitySubtype>> findDeviceTypesByTenantId(TenantId tenantId); 66 ListenableFuture<List<EntitySubtype>> findDeviceTypesByTenantId(TenantId tenantId);
67 67
  68 + Device swapDevice(TenantId tenantId, Device device);
  69 +
68 } 70 }
@@ -41,4 +41,6 @@ public interface EventService { @@ -41,4 +41,6 @@ public interface EventService {
41 41
42 List<Event> findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit); 42 List<Event> findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit);
43 43
  44 + void removeEvents(TenantId tenantId, EntityId entityId);
  45 +
44 } 46 }
@@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.relation.EntityRelationsQuery; @@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
24 import org.thingsboard.server.common.data.relation.RelationTypeGroup; 24 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
25 25
26 import java.util.List; 26 import java.util.List;
27 -import java.util.concurrent.ExecutionException;  
28 27
29 /** 28 /**
30 * Created by ashvayka on 27.04.17. 29 * Created by ashvayka on 27.04.17.
@@ -77,6 +76,8 @@ public interface RelationService { @@ -77,6 +76,8 @@ public interface RelationService {
77 76
78 ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(TenantId tenantId, EntityRelationsQuery query); 77 ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(TenantId tenantId, EntityRelationsQuery query);
79 78
  79 + void removeRelations(TenantId tenantId, EntityId entityId);
  80 +
80 // TODO: This method may be useful for some validations in the future 81 // TODO: This method may be useful for some validations in the future
81 // ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to); 82 // ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to);
82 83
@@ -57,6 +57,8 @@ public class DataConstants { @@ -57,6 +57,8 @@ public class DataConstants {
57 public static final String ATTRIBUTES_DELETED = "ATTRIBUTES_DELETED"; 57 public static final String ATTRIBUTES_DELETED = "ATTRIBUTES_DELETED";
58 public static final String ALARM_ACK = "ALARM_ACK"; 58 public static final String ALARM_ACK = "ALARM_ACK";
59 public static final String ALARM_CLEAR = "ALARM_CLEAR"; 59 public static final String ALARM_CLEAR = "ALARM_CLEAR";
  60 + public static final String ENTITY_SWAPPED_FROM = "ENTITY_SWAPPED_FROM";
  61 + public static final String ENTITY_SWAPPED_TO = "ENTITY_SWAPPED_TO";
60 62
61 public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE"; 63 public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE";
62 64
@@ -40,7 +40,9 @@ public enum ActionType { @@ -40,7 +40,9 @@ public enum ActionType {
40 ALARM_CLEAR(false), 40 ALARM_CLEAR(false),
41 LOGIN(false), 41 LOGIN(false),
42 LOGOUT(false), 42 LOGOUT(false),
43 - LOCKOUT(false); 43 + LOCKOUT(false),
  44 + SWAPPED_FROM_TENANT(false),
  45 + SWAPPED_TO_TENANT(false);
44 46
45 private final boolean isRead; 47 private final boolean isRead;
46 48
@@ -22,11 +22,12 @@ import org.thingsboard.server.common.data.id.CustomerId; @@ -22,11 +22,12 @@ import org.thingsboard.server.common.data.id.CustomerId;
22 import org.thingsboard.server.common.data.id.EntityId; 22 import org.thingsboard.server.common.data.id.EntityId;
23 import org.thingsboard.server.common.data.id.UserId; 23 import org.thingsboard.server.common.data.id.UserId;
24 import org.thingsboard.server.common.data.page.TimePageLink; 24 import org.thingsboard.server.common.data.page.TimePageLink;
  25 +import org.thingsboard.server.dao.Dao;
25 26
26 import java.util.List; 27 import java.util.List;
27 import java.util.UUID; 28 import java.util.UUID;
28 29
29 -public interface AuditLogDao { 30 +public interface AuditLogDao extends Dao<AuditLog> {
30 31
31 ListenableFuture<Void> saveByTenantId(AuditLog auditLog); 32 ListenableFuture<Void> saveByTenantId(AuditLog auditLog);
32 33
@@ -28,7 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired; @@ -28,7 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired;
28 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 28 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
29 import org.springframework.stereotype.Service; 29 import org.springframework.stereotype.Service;
30 import org.springframework.util.StringUtils; 30 import org.springframework.util.StringUtils;
31 -import org.thingsboard.server.common.data.BaseData;  
32 import org.thingsboard.server.common.data.EntityType; 31 import org.thingsboard.server.common.data.EntityType;
33 import org.thingsboard.server.common.data.HasName; 32 import org.thingsboard.server.common.data.HasName;
34 import org.thingsboard.server.common.data.audit.ActionStatus; 33 import org.thingsboard.server.common.data.audit.ActionStatus;
@@ -38,7 +37,6 @@ import org.thingsboard.server.common.data.id.AuditLogId; @@ -38,7 +37,6 @@ import org.thingsboard.server.common.data.id.AuditLogId;
38 import org.thingsboard.server.common.data.id.CustomerId; 37 import org.thingsboard.server.common.data.id.CustomerId;
39 import org.thingsboard.server.common.data.id.EntityId; 38 import org.thingsboard.server.common.data.id.EntityId;
40 import org.thingsboard.server.common.data.id.TenantId; 39 import org.thingsboard.server.common.data.id.TenantId;
41 -import org.thingsboard.server.common.data.id.UUIDBased;  
42 import org.thingsboard.server.common.data.id.UserId; 40 import org.thingsboard.server.common.data.id.UserId;
43 import org.thingsboard.server.common.data.kv.AttributeKvEntry; 41 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
44 import org.thingsboard.server.common.data.page.TimePageData; 42 import org.thingsboard.server.common.data.page.TimePageData;
@@ -53,6 +51,8 @@ import org.thingsboard.server.dao.service.DataValidator; @@ -53,6 +51,8 @@ import org.thingsboard.server.dao.service.DataValidator;
53 51
54 import java.io.PrintWriter; 52 import java.io.PrintWriter;
55 import java.io.StringWriter; 53 import java.io.StringWriter;
  54 +import java.util.ArrayList;
  55 +import java.util.Arrays;
56 import java.util.List; 56 import java.util.List;
57 57
58 import static org.thingsboard.server.dao.service.Validator.validateEntityId; 58 import static org.thingsboard.server.dao.service.Validator.validateEntityId;
@@ -117,8 +117,8 @@ public class AuditLogServiceImpl implements AuditLogService { @@ -117,8 +117,8 @@ public class AuditLogServiceImpl implements AuditLogService {
117 117
118 @Override 118 @Override
119 public <E extends HasName, I extends EntityId> ListenableFuture<List<Void>> 119 public <E extends HasName, I extends EntityId> ListenableFuture<List<Void>>
120 - logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity,  
121 - ActionType actionType, Exception e, Object... additionalInfo) { 120 + logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity,
  121 + ActionType actionType, Exception e, Object... additionalInfo) {
122 if (canLog(entityId.getEntityType(), actionType)) { 122 if (canLog(entityId.getEntityType(), actionType)) {
123 JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo); 123 JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo);
124 ActionStatus actionStatus = ActionStatus.SUCCESS; 124 ActionStatus actionStatus = ActionStatus.SUCCESS;
@@ -129,7 +129,8 @@ public class AuditLogServiceImpl implements AuditLogService { @@ -129,7 +129,8 @@ public class AuditLogServiceImpl implements AuditLogService {
129 } else { 129 } else {
130 try { 130 try {
131 entityName = entityService.fetchEntityNameAsync(tenantId, entityId).get(); 131 entityName = entityService.fetchEntityNameAsync(tenantId, entityId).get();
132 - } catch (Exception ex) {} 132 + } catch (Exception ex) {
  133 + }
133 } 134 }
134 if (e != null) { 135 if (e != null) {
135 actionStatus = ActionStatus.FAILURE; 136 actionStatus = ActionStatus.FAILURE;
@@ -157,16 +158,36 @@ public class AuditLogServiceImpl implements AuditLogService { @@ -157,16 +158,36 @@ public class AuditLogServiceImpl implements AuditLogService {
157 } 158 }
158 } 159 }
159 160
  161 + @Override
  162 + public void removeAuditLogs(TenantId tenantId, EntityId entityId) {
  163 + List<AuditLog> auditLogs = new ArrayList<>();
  164 + TimePageData<AuditLog> auditLogPageData;
  165 + TimePageLink auditLogPageLink = new TimePageLink(1000);
  166 + do {
  167 + auditLogPageData = findAuditLogsByTenantIdAndEntityId(tenantId, entityId,
  168 + new ArrayList<>(Arrays.asList(ActionType.values())), auditLogPageLink);
  169 + auditLogs.addAll(auditLogPageData.getData());
  170 + if (auditLogPageData.hasNext()) {
  171 + auditLogPageLink = auditLogPageData.getNextPageLink();
  172 + }
  173 + } while (auditLogPageData.hasNext());
  174 +
  175 + for (AuditLog auditLog : auditLogs) {
  176 + auditLogDao.removeById(tenantId, auditLog.getUuidId());
  177 + }
  178 + }
  179 +
160 private <E extends HasName, I extends EntityId> JsonNode constructActionData(I entityId, E entity, 180 private <E extends HasName, I extends EntityId> JsonNode constructActionData(I entityId, E entity,
161 - ActionType actionType,  
162 - Object... additionalInfo) { 181 + ActionType actionType,
  182 + Object... additionalInfo) {
163 ObjectNode actionData = objectMapper.createObjectNode(); 183 ObjectNode actionData = objectMapper.createObjectNode();
164 - switch(actionType) { 184 + switch (actionType) {
165 case ADDED: 185 case ADDED:
166 case UPDATED: 186 case UPDATED:
167 case ALARM_ACK: 187 case ALARM_ACK:
168 case ALARM_CLEAR: 188 case ALARM_CLEAR:
169 case RELATIONS_DELETED: 189 case RELATIONS_DELETED:
  190 + case SWAPPED_TO_TENANT:
170 if (entity != null) { 191 if (entity != null) {
171 ObjectNode entityNode = objectMapper.valueToTree(entity); 192 ObjectNode entityNode = objectMapper.valueToTree(entity);
172 if (entityId.getEntityType() == EntityType.DASHBOARD) { 193 if (entityId.getEntityType() == EntityType.DASHBOARD) {
@@ -208,7 +229,7 @@ public class AuditLogServiceImpl implements AuditLogService { @@ -208,7 +229,7 @@ public class AuditLogServiceImpl implements AuditLogService {
208 scope = extractParameter(String.class, 0, additionalInfo); 229 scope = extractParameter(String.class, 0, additionalInfo);
209 actionData.put("scope", scope); 230 actionData.put("scope", scope);
210 List<String> keys = extractParameter(List.class, 1, additionalInfo); 231 List<String> keys = extractParameter(List.class, 1, additionalInfo);
211 - ArrayNode attrsArrayNode = actionData.putArray("attributes"); 232 + ArrayNode attrsArrayNode = actionData.putArray("attributes");
212 if (keys != null) { 233 if (keys != null) {
213 keys.forEach(attrsArrayNode::add); 234 keys.forEach(attrsArrayNode::add);
214 } 235 }
@@ -18,14 +18,12 @@ package org.thingsboard.server.dao.audit; @@ -18,14 +18,12 @@ package org.thingsboard.server.dao.audit;
18 import com.google.common.util.concurrent.ListenableFuture; 18 import com.google.common.util.concurrent.ListenableFuture;
19 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 19 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
20 import org.springframework.stereotype.Service; 20 import org.springframework.stereotype.Service;
21 -import org.thingsboard.server.common.data.BaseData;  
22 import org.thingsboard.server.common.data.HasName; 21 import org.thingsboard.server.common.data.HasName;
23 import org.thingsboard.server.common.data.audit.ActionType; 22 import org.thingsboard.server.common.data.audit.ActionType;
24 import org.thingsboard.server.common.data.audit.AuditLog; 23 import org.thingsboard.server.common.data.audit.AuditLog;
25 import org.thingsboard.server.common.data.id.CustomerId; 24 import org.thingsboard.server.common.data.id.CustomerId;
26 import org.thingsboard.server.common.data.id.EntityId; 25 import org.thingsboard.server.common.data.id.EntityId;
27 import org.thingsboard.server.common.data.id.TenantId; 26 import org.thingsboard.server.common.data.id.TenantId;
28 -import org.thingsboard.server.common.data.id.UUIDBased;  
29 import org.thingsboard.server.common.data.id.UserId; 27 import org.thingsboard.server.common.data.id.UserId;
30 import org.thingsboard.server.common.data.page.TimePageData; 28 import org.thingsboard.server.common.data.page.TimePageData;
31 import org.thingsboard.server.common.data.page.TimePageLink; 29 import org.thingsboard.server.common.data.page.TimePageLink;
@@ -61,4 +59,7 @@ public class DummyAuditLogServiceImpl implements AuditLogService { @@ -61,4 +59,7 @@ public class DummyAuditLogServiceImpl implements AuditLogService {
61 return null; 59 return null;
62 } 60 }
63 61
  62 + @Override
  63 + public void removeAuditLogs(TenantId tenantId, EntityId entityId) {
  64 + }
64 } 65 }
@@ -189,4 +189,14 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt @@ -189,4 +189,14 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
189 }, MoreExecutors.directExecutor()); 189 }, MoreExecutors.directExecutor());
190 } 190 }
191 191
  192 + @Override
  193 + public Device findDeviceByTenantIdAndId(TenantId tenantId, UUID id) {
  194 + return findById(tenantId, id);
  195 + }
  196 +
  197 + @Override
  198 + public ListenableFuture<Device> findDeviceByTenantIdAndIdAsync(TenantId tenantId, UUID id) {
  199 + return findByIdAsync(tenantId, id);
  200 + }
  201 +
192 } 202 }
@@ -115,4 +115,21 @@ public interface DeviceDao extends Dao<Device> { @@ -115,4 +115,21 @@ public interface DeviceDao extends Dao<Device> {
115 * @return the list of tenant device type objects 115 * @return the list of tenant device type objects
116 */ 116 */
117 ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId); 117 ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId);
  118 +
  119 + /**
  120 + * Find devices by tenantId and device id.
  121 + * @param tenantId the tenant Id
  122 + * @param id the device Id
  123 + * @return the device object
  124 + */
  125 + Device findDeviceByTenantIdAndId(TenantId tenantId, UUID id);
  126 +
  127 + /**
  128 + * Find devices by tenantId and device id.
  129 + * @param tenantId tenantId the tenantId
  130 + * @param id the deviceId
  131 + * @return the device object
  132 + */
  133 + ListenableFuture<Device> findDeviceByTenantIdAndIdAsync(TenantId tenantId, UUID id);
  134 +
118 } 135 }
@@ -28,6 +28,8 @@ import org.springframework.cache.CacheManager; @@ -28,6 +28,8 @@ import org.springframework.cache.CacheManager;
28 import org.springframework.cache.annotation.CacheEvict; 28 import org.springframework.cache.annotation.CacheEvict;
29 import org.springframework.cache.annotation.Cacheable; 29 import org.springframework.cache.annotation.Cacheable;
30 import org.springframework.stereotype.Service; 30 import org.springframework.stereotype.Service;
  31 +import org.springframework.transaction.annotation.Transactional;
  32 +import org.springframework.util.CollectionUtils;
31 import org.springframework.util.StringUtils; 33 import org.springframework.util.StringUtils;
32 import org.thingsboard.server.common.data.Customer; 34 import org.thingsboard.server.common.data.Customer;
33 import org.thingsboard.server.common.data.Device; 35 import org.thingsboard.server.common.data.Device;
@@ -46,9 +48,11 @@ import org.thingsboard.server.common.data.relation.EntityRelation; @@ -46,9 +48,11 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
46 import org.thingsboard.server.common.data.relation.EntitySearchDirection; 48 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
47 import org.thingsboard.server.common.data.security.DeviceCredentials; 49 import org.thingsboard.server.common.data.security.DeviceCredentials;
48 import org.thingsboard.server.common.data.security.DeviceCredentialsType; 50 import org.thingsboard.server.common.data.security.DeviceCredentialsType;
  51 +import org.thingsboard.server.dao.audit.AuditLogService;
49 import org.thingsboard.server.dao.customer.CustomerDao; 52 import org.thingsboard.server.dao.customer.CustomerDao;
50 import org.thingsboard.server.dao.entity.AbstractEntityService; 53 import org.thingsboard.server.dao.entity.AbstractEntityService;
51 import org.thingsboard.server.dao.entityview.EntityViewService; 54 import org.thingsboard.server.dao.entityview.EntityViewService;
  55 +import org.thingsboard.server.dao.event.EventService;
52 import org.thingsboard.server.dao.exception.DataValidationException; 56 import org.thingsboard.server.dao.exception.DataValidationException;
53 import org.thingsboard.server.dao.service.DataValidator; 57 import org.thingsboard.server.dao.service.DataValidator;
54 import org.thingsboard.server.dao.service.PaginatedRemover; 58 import org.thingsboard.server.dao.service.PaginatedRemover;
@@ -97,18 +101,32 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -97,18 +101,32 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
97 @Autowired 101 @Autowired
98 private CacheManager cacheManager; 102 private CacheManager cacheManager;
99 103
  104 + @Autowired
  105 + private EventService eventService;
  106 +
  107 + @Autowired
  108 + private AuditLogService auditLogService;
  109 +
100 @Override 110 @Override
101 public Device findDeviceById(TenantId tenantId, DeviceId deviceId) { 111 public Device findDeviceById(TenantId tenantId, DeviceId deviceId) {
102 log.trace("Executing findDeviceById [{}]", deviceId); 112 log.trace("Executing findDeviceById [{}]", deviceId);
103 validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); 113 validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
104 - return deviceDao.findById(tenantId, deviceId.getId()); 114 + if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
  115 + return deviceDao.findById(tenantId, deviceId.getId());
  116 + } else {
  117 + return deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId());
  118 + }
105 } 119 }
106 120
107 @Override 121 @Override
108 public ListenableFuture<Device> findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId) { 122 public ListenableFuture<Device> findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId) {
109 log.trace("Executing findDeviceById [{}]", deviceId); 123 log.trace("Executing findDeviceById [{}]", deviceId);
110 validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); 124 validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
111 - return deviceDao.findByIdAsync(tenantId, deviceId.getId()); 125 + if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
  126 + return deviceDao.findByIdAsync(tenantId, deviceId.getId());
  127 + } else {
  128 + return deviceDao.findDeviceByTenantIdAndIdAsync(tenantId, deviceId.getId());
  129 + }
112 } 130 }
113 131
114 @Cacheable(cacheNames = DEVICE_CACHE, key = "{#tenantId, #name}") 132 @Cacheable(cacheNames = DEVICE_CACHE, key = "{#tenantId, #name}")
@@ -317,6 +335,33 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -317,6 +335,33 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
317 }, MoreExecutors.directExecutor()); 335 }, MoreExecutors.directExecutor());
318 } 336 }
319 337
  338 + @Transactional
  339 + @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}")
  340 + @Override
  341 + public Device swapDevice(TenantId tenantId, Device device) {
  342 + log.trace("Executing swapDevice [{}]", device);
  343 +
  344 + try {
  345 + List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get();
  346 + if (!CollectionUtils.isEmpty(entityViews)) {
  347 + throw new DataValidationException("Can't swap device that is assigned to entity views!");
  348 + }
  349 + } catch (ExecutionException | InterruptedException e) {
  350 + log.error("Exception while finding entity views for deviceId [{}]", device.getId(), e);
  351 + throw new RuntimeException("Exception while finding entity views for deviceId [" + device.getId() + "]", e);
  352 + }
  353 +
  354 + eventService.removeEvents(device.getTenantId(), device.getId());
  355 +
  356 + relationService.removeRelations(device.getTenantId(), device.getId());
  357 +
  358 + auditLogService.removeAuditLogs(device.getTenantId(), device.getId());
  359 +
  360 + device.setTenantId(tenantId);
  361 + device.setCustomerId(null);
  362 + return doSaveDevice(device, null);
  363 + }
  364 +
320 private DataValidator<Device> deviceValidator = 365 private DataValidator<Device> deviceValidator =
321 new DataValidator<Device>() { 366 new DataValidator<Device>() {
322 367
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.page.TimePageLink; @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.page.TimePageLink;
28 import org.thingsboard.server.dao.exception.DataValidationException; 28 import org.thingsboard.server.dao.exception.DataValidationException;
29 import org.thingsboard.server.dao.service.DataValidator; 29 import org.thingsboard.server.dao.service.DataValidator;
30 30
  31 +import java.util.ArrayList;
31 import java.util.List; 32 import java.util.List;
32 import java.util.Optional; 33 import java.util.Optional;
33 34
@@ -94,6 +95,24 @@ public class BaseEventService implements EventService { @@ -94,6 +95,24 @@ public class BaseEventService implements EventService {
94 return eventDao.findLatestEvents(tenantId.getId(), entityId, eventType, limit); 95 return eventDao.findLatestEvents(tenantId.getId(), entityId, eventType, limit);
95 } 96 }
96 97
  98 + @Override
  99 + public void removeEvents(TenantId tenantId, EntityId entityId) {
  100 + List<Event> events = new ArrayList<>();
  101 + TimePageData<Event> eventPageData;
  102 + TimePageLink eventPageLink = new TimePageLink(1000);
  103 + do {
  104 + eventPageData = findEvents(tenantId, entityId, eventPageLink);
  105 + events.addAll(eventPageData.getData());
  106 + if (eventPageData.hasNext()) {
  107 + eventPageLink = eventPageData.getNextPageLink();
  108 + }
  109 + } while (eventPageData.hasNext());
  110 +
  111 + for (Event event : events) {
  112 + eventDao.removeById(tenantId, event.getUuidId());
  113 + }
  114 + }
  115 +
97 private DataValidator<Event> eventValidator = 116 private DataValidator<Event> eventValidator =
98 new DataValidator<Event>() { 117 new DataValidator<Event>() {
99 @Override 118 @Override
@@ -506,6 +506,22 @@ public class BaseRelationService implements RelationService { @@ -506,6 +506,22 @@ public class BaseRelationService implements RelationService {
506 }, MoreExecutors.directExecutor()); 506 }, MoreExecutors.directExecutor());
507 } 507 }
508 508
  509 + @Override
  510 + public void removeRelations(TenantId tenantId, EntityId entityId) {
  511 + Cache cache = cacheManager.getCache(RELATIONS_CACHE);
  512 +
  513 + List<EntityRelation> relations = new ArrayList<>();
  514 + for (RelationTypeGroup relationTypeGroup : RelationTypeGroup.values()) {
  515 + relations.addAll(findByFrom(tenantId, entityId, relationTypeGroup));
  516 + relations.addAll(findByTo(tenantId, entityId, relationTypeGroup));
  517 + }
  518 +
  519 + for (EntityRelation relation : relations) {
  520 + cacheEviction(relation, cache);
  521 + deleteRelation(tenantId, relation);
  522 + }
  523 + }
  524 +
509 protected void validate(EntityRelation relation) { 525 protected void validate(EntityRelation relation) {
510 if (relation == null) { 526 if (relation == null) {
511 throw new DataValidationException("Relation type should be specified!"); 527 throw new DataValidationException("Relation type should be specified!");
@@ -85,4 +85,7 @@ public interface DeviceRepository extends CrudRepository<DeviceEntity, String> { @@ -85,4 +85,7 @@ public interface DeviceRepository extends CrudRepository<DeviceEntity, String> {
85 List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndIdIn(String tenantId, String customerId, List<String> deviceIds); 85 List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndIdIn(String tenantId, String customerId, List<String> deviceIds);
86 86
87 List<DeviceEntity> findDevicesByTenantIdAndIdIn(String tenantId, List<String> deviceIds); 87 List<DeviceEntity> findDevicesByTenantIdAndIdIn(String tenantId, List<String> deviceIds);
  88 +
  89 + DeviceEntity findByTenantIdAndId(String tenantId, String id);
  90 +
88 } 91 }
@@ -138,6 +138,16 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device> @@ -138,6 +138,16 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
138 return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId)))); 138 return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId))));
139 } 139 }
140 140
  141 + @Override
  142 + public Device findDeviceByTenantIdAndId(TenantId tenantId, UUID id) {
  143 + return DaoUtil.getData(deviceRepository.findByTenantIdAndId(fromTimeUUID(tenantId.getId()), fromTimeUUID(id)));
  144 + }
  145 +
  146 + @Override
  147 + public ListenableFuture<Device> findDeviceByTenantIdAndIdAsync(TenantId tenantId, UUID id) {
  148 + return service.submit(() -> DaoUtil.getData(deviceRepository.findByTenantIdAndId(fromTimeUUID(tenantId.getId()), fromTimeUUID(id))));
  149 + }
  150 +
141 private List<EntitySubtype> convertTenantDeviceTypesToDto(UUID tenantId, List<String> types) { 151 private List<EntitySubtype> convertTenantDeviceTypesToDto(UUID tenantId, List<String> types) {
142 List<EntitySubtype> list = Collections.emptyList(); 152 List<EntitySubtype> list = Collections.emptyList();
143 if (types != null && !types.isEmpty()) { 153 if (types != null && !types.isEmpty()) {
@@ -16,8 +16,13 @@ @@ -16,8 +16,13 @@
16 package org.thingsboard.rule.engine.filter; 16 package org.thingsboard.rule.engine.filter;
17 17
18 import lombok.extern.slf4j.Slf4j; 18 import lombok.extern.slf4j.Slf4j;
  19 +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
  20 +import org.thingsboard.rule.engine.api.RuleNode;
  21 +import org.thingsboard.rule.engine.api.TbContext;
  22 +import org.thingsboard.rule.engine.api.TbNode;
  23 +import org.thingsboard.rule.engine.api.TbNodeConfiguration;
  24 +import org.thingsboard.rule.engine.api.TbNodeException;
19 import org.thingsboard.rule.engine.api.util.TbNodeUtils; 25 import org.thingsboard.rule.engine.api.util.TbNodeUtils;
20 -import org.thingsboard.rule.engine.api.*;  
21 import org.thingsboard.server.common.data.DataConstants; 26 import org.thingsboard.server.common.data.DataConstants;
22 import org.thingsboard.server.common.data.plugin.ComponentType; 27 import org.thingsboard.server.common.data.plugin.ComponentType;
23 import org.thingsboard.server.common.msg.TbMsg; 28 import org.thingsboard.server.common.msg.TbMsg;
@@ -30,7 +35,7 @@ import org.thingsboard.server.common.msg.session.SessionMsgType; @@ -30,7 +35,7 @@ import org.thingsboard.server.common.msg.session.SessionMsgType;
30 configClazz = EmptyNodeConfiguration.class, 35 configClazz = EmptyNodeConfiguration.class,
31 relationTypes = {"Post attributes", "Post telemetry", "RPC Request from Device", "RPC Request to Device", "Activity Event", "Inactivity Event", 36 relationTypes = {"Post attributes", "Post telemetry", "RPC Request from Device", "RPC Request to Device", "Activity Event", "Inactivity Event",
32 "Connect Event", "Disconnect Event", "Entity Created", "Entity Updated", "Entity Deleted", "Entity Assigned", 37 "Connect Event", "Disconnect Event", "Entity Created", "Entity Updated", "Entity Deleted", "Entity Assigned",
33 - "Entity Unassigned", "Attributes Updated", "Attributes Deleted", "Alarm Acknowledged", "Alarm Cleared", "Other"}, 38 + "Entity Unassigned", "Attributes Updated", "Attributes Deleted", "Alarm Acknowledged", "Alarm Cleared", "Other", "Entity Swapped From", "Entity Swapped To"},
34 nodeDescription = "Route incoming messages by Message Type", 39 nodeDescription = "Route incoming messages by Message Type",
35 nodeDetails = "Sends messages with message types <b>\"Post attributes\", \"Post telemetry\", \"RPC Request\"</b> etc. via corresponding chain, otherwise <b>Other</b> chain is used.", 40 nodeDetails = "Sends messages with message types <b>\"Post attributes\", \"Post telemetry\", \"RPC Request\"</b> etc. via corresponding chain, otherwise <b>Other</b> chain is used.",
36 uiResources = {"static/rulenode/rulenode-core-config.js"}, 41 uiResources = {"static/rulenode/rulenode-core-config.js"},
@@ -81,6 +86,10 @@ public class TbMsgTypeSwitchNode implements TbNode { @@ -81,6 +86,10 @@ public class TbMsgTypeSwitchNode implements TbNode {
81 relationType = "Alarm Cleared"; 86 relationType = "Alarm Cleared";
82 } else if (msg.getType().equals(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE)) { 87 } else if (msg.getType().equals(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE)) {
83 relationType = "RPC Request to Device"; 88 relationType = "RPC Request to Device";
  89 + } else if (msg.getType().equals(DataConstants.ENTITY_SWAPPED_FROM)) {
  90 + relationType = "Entity Swapped From";
  91 + } else if (msg.getType().equals(DataConstants.ENTITY_SWAPPED_TO)) {
  92 + relationType = "Entity Swapped To";
84 } else { 93 } else {
85 relationType = "Other"; 94 relationType = "Other";
86 } 95 }
@@ -222,6 +222,12 @@ export default angular.module('thingsboard.types', []) @@ -222,6 +222,12 @@ export default angular.module('thingsboard.types', [])
222 }, 222 },
223 "LOCKOUT": { 223 "LOCKOUT": {
224 name: "audit-log.type-lockout" 224 name: "audit-log.type-lockout"
  225 + },
  226 + "SWAPPED_FROM_TENANT": {
  227 + name: "audit-log.type-swapped-from-tenant"
  228 + },
  229 + "SWAPPED_TO_TENANT": {
  230 + name: "audit-log.type-swapped-to-tenant"
225 } 231 }
226 }, 232 },
227 auditLogActionStatus: { 233 auditLogActionStatus: {
@@ -356,7 +356,9 @@ @@ -356,7 +356,9 @@
356 "action-data": "Action data", 356 "action-data": "Action data",
357 "failure-details": "Failure details", 357 "failure-details": "Failure details",
358 "search": "Search audit logs", 358 "search": "Search audit logs",
359 - "clear-search": "Clear search" 359 + "clear-search": "Clear search",
  360 + "type-swapped-from-tenant": "Swapped from Tenant",
  361 + "type-swapped-to-tenant": "Swapped to Tenant"
360 }, 362 },
361 "confirm-on-exit": { 363 "confirm-on-exit": {
362 "message": "You have unsaved changes. Are you sure you want to leave this page?", 364 "message": "You have unsaved changes. Are you sure you want to leave this page?",