diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 0942329f40..f9d1fe7a2f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -22,6 +22,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.ConstraintViolation; import lombok.Getter; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.hibernate.exception.ConstraintViolationException; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -358,29 +359,33 @@ public abstract class BaseController { private ThingsboardException handleException(Exception exception, boolean logException) { if (logException && logControllerErrorStackTrace) { - log.error("Error [{}]", exception.getMessage(), exception); - } - - String cause = ""; - if (exception.getCause() != null) { - cause = exception.getCause().getClass().getCanonicalName(); + try { + SecurityUser user = getCurrentUser(); + log.error("[{}][{}] Error", user.getTenantId(), user.getId(), exception); + } catch (Exception e) { + log.error("Error", exception); + } } + Throwable cause = exception.getCause(); if (exception instanceof ThingsboardException) { return (ThingsboardException) exception; } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException - || exception instanceof DataValidationException || cause.contains("IncorrectParameterException")) { + || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); } else if (exception instanceof MessagingException) { return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL); } else if (exception instanceof AsyncRequestTimeoutException) { return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL); } else if (exception instanceof DataAccessException) { - String errorType = exception.getClass().getSimpleName(); if (!logControllerErrorStackTrace) { // not to log the error twice - log.warn("Database error: {} - {}", errorType, ExceptionUtils.getRootCauseMessage(exception)); + log.warn("Database error: {} - {}", exception.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(exception)); + } + if (cause instanceof ConstraintViolationException) { + return new ThingsboardException(ExceptionUtils.getRootCause(exception).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } else { + return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL); } - return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL); } return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 651339e3a4..11974d2ebc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -204,6 +204,9 @@ public class EdgeEventSourcingListener { return false; } } + if (entity instanceof OAuth2Info oAuth2Info) { + return oAuth2Info.isEdgeEnabled(); + } // Default: If the entity doesn't match any of the conditions, consider it as valid. return true; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java index 4412f91183..982c1a8eaf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java @@ -45,8 +45,11 @@ public class OAuth2EdgeEventFetcher implements EdgeEventFetcher { @Override public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { - List result = new ArrayList<>(); OAuth2Info oAuth2Info = oAuth2Service.findOAuth2Info(); + if (!oAuth2Info.isEdgeEnabled()) { + return new PageData<>(); + } + List result = new ArrayList<>(); result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.OAUTH2, EdgeEventActionType.ADDED, null, JacksonUtil.valueToTree(oAuth2Info))); // returns PageData object to be in sync with other fetchers diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java index ce0600b491..adfb6f7d9f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java @@ -40,7 +40,7 @@ public class OAuth2EdgeProcessor extends BaseEdgeProcessor { public DownlinkMsg convertOAuth2EventToDownlink(EdgeEvent edgeEvent) { DownlinkMsg downlinkMsg = null; OAuth2Info oAuth2Info = JacksonUtil.convertValue(edgeEvent.getBody(), OAuth2Info.class); - if (oAuth2Info != null) { + if (oAuth2Info != null && oAuth2Info.isEdgeEnabled()) { OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2MsgConstructor.constructOAuth2UpdateMsg(oAuth2Info); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 383d4d2901..483c0d4688 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -24,12 +24,14 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasRuleEngineProfile; +import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; @@ -340,29 +342,33 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) { - TenantId tenantId = resource.getTenantId(); - log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); - TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setResourceType(resource.getResourceType().name()) - .setResourceKey(resource.getResourceKey()) - .build(); - ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build(); - broadcast(transportMsg, callback); + if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { + TenantId tenantId = resource.getTenantId(); + log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); + TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setResourceType(resource.getResourceType().name()) + .setResourceKey(resource.getResourceKey()) + .build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build(); + broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); + } } @Override public void onResourceDeleted(TbResourceInfo resource, TbQueueCallback callback) { - log.trace("[{}] Processing delete resource", resource); - TransportProtos.ResourceDeleteMsg resourceUpdateMsg = TransportProtos.ResourceDeleteMsg.newBuilder() - .setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits()) - .setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits()) - .setResourceType(resource.getResourceType().name()) - .setResourceKey(resource.getResourceKey()) - .build(); - ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceUpdateMsg).build(); - broadcast(transportMsg, callback); + if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { + log.trace("[{}][{}][{}] Processing delete resource", resource.getTenantId(), resource.getResourceType(), resource.getResourceKey()); + TransportProtos.ResourceDeleteMsg resourceDeleteMsg = TransportProtos.ResourceDeleteMsg.newBuilder() + .setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits()) + .setResourceType(resource.getResourceType().name()) + .setResourceKey(resource.getResourceKey()) + .build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build(); + broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); + } } private void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { @@ -384,8 +390,19 @@ public class DefaultTbClusterService implements TbClusterService { } private void broadcast(ToTransportMsg transportMsg, TbQueueCallback callback) { - TbQueueProducer> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer(); Set tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT); + broadcast(transportMsg, tbTransportServices, callback); + } + + private void broadcast(ToTransportMsg transportMsg, String transportType, TbQueueCallback callback) { + Set tbTransportServices = partitionService.getAllServices(ServiceType.TB_TRANSPORT).stream() + .filter(info -> info.getTransportsList().contains(transportType)) + .map(TransportProtos.ServiceInfo::getServiceId).collect(Collectors.toSet()); + broadcast(transportMsg, tbTransportServices, callback); + } + + private void broadcast(ToTransportMsg transportMsg, Set tbTransportServices, TbQueueCallback callback) { + TbQueueProducer> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer(); TbQueueCallback proxyCallback = callback != null ? new MultipleTbQueueCallbackWrapper(tbTransportServices.size(), callback) : null; for (String transportServiceId : tbTransportServices) { TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId); diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java index 7e1b9b30c9..d427c26d05 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java @@ -27,13 +27,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; @@ -48,6 +51,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -547,6 +551,33 @@ public class DashboardControllerTest extends AbstractControllerTest { testEntityDaoWithRelationsTransactionalException(dashboardDao, savedTenant.getId(), dashboardId, "/api/dashboard/" + dashboardId); } + @Test + public void whenDeletingDashboard_ifReferencedByDeviceProfile_thenReturnError() throws Exception { + Dashboard dashboard = createDashboard("test"); + DeviceProfile deviceProfile = createDeviceProfile("test"); + deviceProfile.setDefaultDashboardId(dashboard.getId()); + doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + + String response = doDelete("/api/dashboard/" + dashboard.getUuidId()).andExpect(status().isBadRequest()) + .andReturn().getResponse().getContentAsString(); + String errorMessage = JacksonUtil.toJsonNode(response).get("message").asText(); + assertThat(errorMessage).containsIgnoringCase("referenced by a device profile"); + } + + @Test + public void whenDeletingDashboard_ifReferencedByAssetProfile_thenReturnError() throws Exception { + Dashboard dashboard = createDashboard("test"); + AssetProfile assetProfile = createAssetProfile("test"); + assetProfile.setDefaultDashboardId(dashboard.getId()); + doPost("/api/assetProfile", assetProfile, AssetProfile.class); + + String response = doDelete("/api/dashboard/" + dashboard.getUuidId()).andExpect(status().isBadRequest()) + .andReturn().getResponse().getContentAsString(); + String errorMessage = JacksonUtil.toJsonNode(response).get("message").asText(); + assertThat(errorMessage).containsIgnoringCase("referenced by an asset profile"); + + } + private Dashboard createDashboard(String title) { Dashboard dashboard = new Dashboard(); dashboard.setTitle(title); diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index 3160d542f1..14e597378c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -616,6 +616,14 @@ public class TenantControllerTest extends AbstractControllerTest { assertThat(usedTpi.getTopic()).isEqualTo(DataConstants.HP_QUEUE_TOPIC); assertThat(usedTpi.getTenantId()).get().isEqualTo(TenantId.SYS_TENANT_ID); }); + assertThat(partitionService.resolve(ServiceType.TB_RULE_ENGINE, null, tenantId, tenantId)).satisfies(tpi -> { + assertThat(tpi.getTopic()).isEqualTo(MAIN_QUEUE_TOPIC); + assertThat(tpi.getTenantId()).get().isEqualTo(tenantId); + }); + assertThat(partitionService.resolve(ServiceType.TB_RULE_ENGINE, "", tenantId, tenantId)).satisfies(tpi -> { + assertThat(tpi.getTopic()).isEqualTo(MAIN_QUEUE_TOPIC); + assertThat(tpi.getTenantId()).get().isEqualTo(tenantId); + }); loginSysAdmin(); tenantProfile.setIsolatedTbRuleEngine(true); @@ -850,4 +858,5 @@ public class TenantControllerTest extends AbstractControllerTest { testBroadcastEntityStateChangeEventNever(createEntityId_NULL_UUID(new Tenant())); Mockito.reset(tbClusterService); } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 04d55f91c0..53a2152782 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.queue.discovery; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; +import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -36,7 +37,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.ServiceListChangedEvent; import org.thingsboard.server.queue.util.AfterStartUp; -import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -319,7 +319,7 @@ public class HashPartitionService implements PartitionService { private QueueKey getQueueKey(ServiceType serviceType, String queueName, TenantId tenantId) { TenantId isolatedOrSystemTenantId = getIsolatedOrSystemTenantId(serviceType, tenantId); - if (queueName == null) { + if (queueName == null || queueName.isEmpty()) { queueName = MAIN_QUEUE_NAME; } QueueKey queueKey = new QueueKey(serviceType, queueName, isolatedOrSystemTenantId); @@ -672,6 +672,7 @@ public class HashPartitionService implements PartitionService { public QueueConfig(QueueRoutingInfo queueRoutingInfo) { this.duplicateMsgToAllPartitions = queueRoutingInfo.isDuplicateMsgToAllPartitions(); } + } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index cc91baca0c..8e48061db6 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -948,7 +948,7 @@ public class DefaultTransportService extends TransportActivityManager implements String resourceId = msg.getResourceKey(); transportResourceCache.evict(tenantId, resourceType, resourceId); sessions.forEach((id, mdRez) -> { - log.warn("ResourceDelete - [{}] [{}]", id, mdRez); + log.trace("ResourceDelete - [{}] [{}]", id, mdRez); transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceDelete(msg)); }); } else if (toSessionMsg.getQueueUpdateMsgsCount() > 0) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 6a0de28bf2..5c7430ebc8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.dashboard; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -57,6 +56,7 @@ import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.sql.JpaExecutorService; import java.util.List; +import java.util.Map; import java.util.Optional; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -235,13 +235,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb publishEvictEvent(new DashboardTitleEvictEvent(dashboardId)); countService.publishCountEntityEvictEvent(tenantId, EntityType.DASHBOARD); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(dashboardId).build()); - } catch (Exception t) { - ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); - if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_dashboard_device_profile")) { - throw new DataValidationException("The dashboard referenced by the device profiles cannot be deleted!"); - } else { - throw t; - } + } catch (Exception e) { + checkConstraintViolation(e, Map.of( + "fk_default_dashboard_device_profile", "The dashboard is referenced by a device profile", + "fk_default_dashboard_asset_profile", "The dashboard is referenced by an asset profile" + )); + throw e; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java index ac1741d044..329fa46c3b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java @@ -181,8 +181,11 @@ public class CalculateDeltaNode implements TbNode { private ListenableFuture getLatestFromCacheOrFetchFromDb(TbContext ctx, TbMsg msg) { EntityId originator = msg.getOriginator(); - ValueWithTs valueWithTs = cache.get(msg.getOriginator()); - return valueWithTs != null ? Futures.immediateFuture(valueWithTs) : fetchLatestValueAsync(ctx, originator); + if (config.isUseCache()) { + ValueWithTs valueWithTs = cache.get(msg.getOriginator()); + return valueWithTs != null ? Futures.immediateFuture(valueWithTs) : fetchLatestValueAsync(ctx, originator); + } + return fetchLatestValueAsync(ctx, originator); } private record ValueWithTs(long ts, double value) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 5074024be5..2baf26206f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -53,6 +53,7 @@ import java.util.concurrent.TimeoutException; type = ComponentType.EXTERNAL, name = "mqtt", configClazz = TbMqttNodeConfiguration.class, + version = 1, clusteringMode = ComponentClusteringMode.USER_PREFERENCE, nodeDescription = "Publish messages to the MQTT broker", nodeDetails = "Will publish message payload to the MQTT broker with QoS AT_LEAST_ONCE.", diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java index c427dcbd3b..1b2ff4f2bf 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java @@ -109,7 +109,6 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { public void setUp() throws TbNodeException { config = new CalculateDeltaNodeConfiguration().defaultConfiguration(); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctxMock, nodeConfiguration); } @Test @@ -166,8 +165,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { } @Test - public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() { + public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() throws TbNodeException { // GIVEN + node.init(ctxMock, nodeConfiguration); var msgData = "{\"pulseCounter\": 42}"; var msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); @@ -181,8 +181,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { } @Test - public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() { + public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() throws TbNodeException { // GIVEN + node.init(ctxMock, nodeConfiguration); var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN @@ -196,8 +197,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { @Test - public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() { + public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() throws TbNodeException { // GIVEN + node.init(ctxMock, nodeConfiguration); var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); // WHEN @@ -451,8 +453,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { } @Test - public void givenInvalidStringValue_whenOnMsg_thenException() { + public void givenInvalidStringValue_whenOnMsg_thenException() throws TbNodeException { // GIVEN + node.init(ctxMock, nodeConfiguration); mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high"))); var msgData = "{\"pulseCounter\":\"123\"}"; @@ -475,8 +478,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { } @Test - public void givenBooleanValue_whenOnMsg_thenException() { + public void givenBooleanValue_whenOnMsg_thenException() throws TbNodeException { // GIVEN + node.init(ctxMock, nodeConfiguration); mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false))); var msgData = "{\"pulseCounter\":true}"; @@ -499,8 +503,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { } @Test - public void givenJsonValue_whenOnMsg_thenException() { + public void givenJsonValue_whenOnMsg_thenException() throws TbNodeException { // GIVEN + node.init(ctxMock, nodeConfiguration); mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}"))); var msgData = "{\"pulseCounter\":{\"isActive\":true}}"; diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index dc9ed5004b..a534f157b7 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -364,6 +364,16 @@ export function mergeDeep(target: T, ...sources: T[]): T { return _.merge(target, ...sources); } +function ignoreArrayMergeFunc(target: any, sources: any) { + if (_.isArray(target)) { + return sources; + } +} + +export function mergeDeepIgnoreArray(target: T, ...sources: T[]): T { + return _.mergeWith(target, ...sources, ignoreArrayMergeFunc); +} + export function guid(): string { function s4(): string { return Math.floor((1 + Math.random()) * 0x10000) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts index 8e29d43a3e..85666ef91e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts @@ -33,7 +33,7 @@ import { getTimewindowConfig, setTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; -import { formatValue, isUndefined, mergeDeep } from '@core/utils'; +import { formatValue, isUndefined, mergeDeepIgnoreArray } from '@core/utils'; import { cssSizeToStrSize, DateFormatProcessor, @@ -117,7 +117,7 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { } protected onConfigSet(configData: WidgetConfigComponentData) { - const settings: RangeChartWidgetSettings = mergeDeep({} as RangeChartWidgetSettings, + const settings: RangeChartWidgetSettings = mergeDeepIgnoreArray({} as RangeChartWidgetSettings, rangeChartDefaultSettings, configData.config.settings as RangeChartWidgetSettings); const iconSize = resolveCssSize(configData.config.iconSize); this.rangeChartWidgetConfigForm = this.fb.group({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts index 2dd34a10d7..9392c9f277 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts @@ -25,7 +25,7 @@ import { import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { formatValue, mergeDeep } from '@core/utils'; +import { formatValue, mergeDeepIgnoreArray } from '@core/utils'; import { rangeChartDefaultSettings, RangeChartWidgetSettings @@ -99,7 +99,7 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { } protected defaultSettings(): WidgetSettings { - return mergeDeep({} as RangeChartWidgetSettings, rangeChartDefaultSettings); + return mergeDeepIgnoreArray({} as RangeChartWidgetSettings, rangeChartDefaultSettings); } protected onSettingsSet(settings: WidgetSettings) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts index fcfd5f47e5..aa69b05d75 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts @@ -139,7 +139,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On } else { rangeList = deepClone(value); } - this.colorRangeListFormGroup.get('advancedMode').patchValue(rangeList.advancedMode, {emitEvent: false}); + this.colorRangeListFormGroup.get('advancedMode').patchValue(rangeList.advancedMode || false, {emitEvent: false}); if (isDefinedAndNotNull(rangeList?.range)) { rangeList.range.forEach((r) => this.rangeListFormArray.push(this.colorRangeControl(r), {emitEvent: false})); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.ts index b4baf12dad..18ec18963f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.ts @@ -16,7 +16,7 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; -import { ColorRange } from '@shared/models/widget-settings.models'; +import { ColorRange, ColorRangeSettings } from '@shared/models/widget-settings.models'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; @@ -71,8 +71,8 @@ export class ColorRangePanelComponent extends PageComponent implements OnInit { } applyColorRangeSettings() { - const colorRangeSettings = this.colorRangeFormGroup.get('rangeList').value; - this.colorRangeApplied.emit(colorRangeSettings); + const colorRangeSettings: ColorRangeSettings = this.colorRangeFormGroup.get('rangeList').value; + this.colorRangeApplied.emit(colorRangeSettings.range); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts index 5770f45c99..54c8be65b8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts @@ -25,7 +25,7 @@ import { ViewContainerRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ColorRange, ComponentStyle } from '@shared/models/widget-settings.models'; +import { ColorRange, ColorRangeSettings, ComponentStyle } from '@shared/models/widget-settings.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { ColorRangePanelComponent } from '@home/components/widget/lib/settings/common/color-range-panel.component'; @@ -108,8 +108,8 @@ export class ColorRangeSettingsComponent implements OnInit, ControlValueAccessor this.updateColorStyle(); } - writeValue(value: Array): void { - this.modelValue = value; + writeValue(value: Array | ColorRangeSettings): void { + this.modelValue = Array.isArray(value) ? value : value.range; this.updateColorStyle(); } @@ -131,7 +131,7 @@ export class ColorRangeSettingsComponent implements OnInit, ControlValueAccessor {}, {}, {}, true); colorRangeSettingsPanelPopover.tbComponentRef.instance.popover = colorRangeSettingsPanelPopover; - colorRangeSettingsPanelPopover.tbComponentRef.instance.colorRangeApplied.subscribe((colorRangeSettings) => { + colorRangeSettingsPanelPopover.tbComponentRef.instance.colorRangeApplied.subscribe((colorRangeSettings: Array) => { colorRangeSettingsPanelPopover.hide(); this.modelValue = colorRangeSettings; this.updateColorStyle(); diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html index 1a09d539c8..67727591a4 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.html +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -18,7 +18,7 @@
-
+