Merge remote-tracking branch 'origin/hotfix/3.7'
This commit is contained in:
		
						commit
						68c8bf8db7
					
				@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -45,8 +45,11 @@ public class OAuth2EdgeEventFetcher implements EdgeEventFetcher {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
 | 
			
		||||
        List<EdgeEvent> result = new ArrayList<>();
 | 
			
		||||
        OAuth2Info oAuth2Info = oAuth2Service.findOAuth2Info();
 | 
			
		||||
        if (!oAuth2Info.isEdgeEnabled()) {
 | 
			
		||||
            return new PageData<>();
 | 
			
		||||
        }
 | 
			
		||||
        List<EdgeEvent> 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
 | 
			
		||||
 | 
			
		||||
@ -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())
 | 
			
		||||
 | 
			
		||||
@ -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 <T> 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<TbProtoQueueMsg<ToTransportMsg>> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer();
 | 
			
		||||
        Set<String> tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT);
 | 
			
		||||
        broadcast(transportMsg, tbTransportServices, callback);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void broadcast(ToTransportMsg transportMsg, String transportType, TbQueueCallback callback) {
 | 
			
		||||
        Set<String> 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<String> tbTransportServices, TbQueueCallback callback) {
 | 
			
		||||
        TbQueueProducer<TbProtoQueueMsg<ToTransportMsg>> 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);
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -181,8 +181,11 @@ public class CalculateDeltaNode implements TbNode {
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<ValueWithTs> 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) {
 | 
			
		||||
 | 
			
		||||
@ -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 <b>AT_LEAST_ONCE</b>.",
 | 
			
		||||
 | 
			
		||||
@ -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}}";
 | 
			
		||||
 | 
			
		||||
@ -364,6 +364,16 @@ export function mergeDeep<T>(target: T, ...sources: T[]): T {
 | 
			
		||||
  return _.merge(target, ...sources);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ignoreArrayMergeFunc(target: any, sources: any) {
 | 
			
		||||
  if (_.isArray(target)) {
 | 
			
		||||
    return sources;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function mergeDeepIgnoreArray<T>(target: T, ...sources: T[]): T {
 | 
			
		||||
  return _.mergeWith(target, ...sources, ignoreArrayMergeFunc);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function guid(): string {
 | 
			
		||||
  function s4(): string {
 | 
			
		||||
    return Math.floor((1 + Math.random()) * 0x10000)
 | 
			
		||||
 | 
			
		||||
@ -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<RangeChartWidgetSettings>({} as RangeChartWidgetSettings,
 | 
			
		||||
    const settings: RangeChartWidgetSettings = mergeDeepIgnoreArray<RangeChartWidgetSettings>({} as RangeChartWidgetSettings,
 | 
			
		||||
      rangeChartDefaultSettings, configData.config.settings as RangeChartWidgetSettings);
 | 
			
		||||
    const iconSize = resolveCssSize(configData.config.iconSize);
 | 
			
		||||
    this.rangeChartWidgetConfigForm = this.fb.group({
 | 
			
		||||
 | 
			
		||||
@ -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<RangeChartWidgetSettings>({} as RangeChartWidgetSettings, rangeChartDefaultSettings);
 | 
			
		||||
    return mergeDeepIgnoreArray<RangeChartWidgetSettings>({} as RangeChartWidgetSettings, rangeChartDefaultSettings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected onSettingsSet(settings: WidgetSettings) {
 | 
			
		||||
 | 
			
		||||
@ -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}));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<ColorRange>): void {
 | 
			
		||||
    this.modelValue = value;
 | 
			
		||||
  writeValue(value: Array<ColorRange> | 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<ColorRange>) => {
 | 
			
		||||
        colorRangeSettingsPanelPopover.hide();
 | 
			
		||||
        this.modelValue = colorRangeSettings;
 | 
			
		||||
        this.updateColorStyle();
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@
 | 
			
		||||
<div class="tb-json-content" style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
 | 
			
		||||
     tb-fullscreen
 | 
			
		||||
     [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
 | 
			
		||||
  <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar" *ngIf="hideToolbar">
 | 
			
		||||
  <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar" *ngIf="!hideToolbar">
 | 
			
		||||
    <label class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && (!contentValid || required && !contentBody), 'tb-required': !disabled && required}">{{ label }}</label>
 | 
			
		||||
    <span fxFlex></span>
 | 
			
		||||
    <button type="button"
 | 
			
		||||
 | 
			
		||||
@ -655,7 +655,7 @@ export enum SnmpAuthenticationProtocol {
 | 
			
		||||
  SHA_256 = 'SHA_256',
 | 
			
		||||
  SHA_384 = 'SHA_384',
 | 
			
		||||
  SHA_512 = 'SHA_512',
 | 
			
		||||
  MD5 = 'MD%'
 | 
			
		||||
  MD5 = 'MD5'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SnmpAuthenticationProtocolTranslationMap = new Map<SnmpAuthenticationProtocol, string>([
 | 
			
		||||
 | 
			
		||||
@ -195,8 +195,8 @@ export const colorRangeIncludes = (range: ColorRange, toCheck: ColorRange): bool
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const filterIncludingColorRanges = (ranges: Array<ColorRange>): Array<ColorRange> => {
 | 
			
		||||
  const result = [...ranges];
 | 
			
		||||
export const filterIncludingColorRanges = (ranges: Array<ColorRange> | ColorRangeSettings): Array<ColorRange> => {
 | 
			
		||||
  const result = [...(Array.isArray(ranges) ? ranges : ranges.range)];
 | 
			
		||||
  let includes = true;
 | 
			
		||||
  while (includes) {
 | 
			
		||||
    let index = -1;
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ import { AbstractControl, UntypedFormGroup } from '@angular/forms';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { Dashboard } from '@shared/models/dashboard.models';
 | 
			
		||||
import { IAliasController } from '@core/api/widget-api.models';
 | 
			
		||||
import { isNotEmptyStr, mergeDeep } from '@core/utils';
 | 
			
		||||
import { isNotEmptyStr, mergeDeepIgnoreArray } from '@core/utils';
 | 
			
		||||
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
 | 
			
		||||
import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models';
 | 
			
		||||
import { NULL_UUID } from '@shared/models/id/has-uuid';
 | 
			
		||||
@ -878,7 +878,7 @@ export abstract class WidgetSettingsComponent extends PageComponent implements
 | 
			
		||||
    if (!value) {
 | 
			
		||||
      this.settingsValue = this.defaultSettings();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.settingsValue = mergeDeep(this.defaultSettings(), value);
 | 
			
		||||
      this.settingsValue = mergeDeepIgnoreArray(this.defaultSettings(), value);
 | 
			
		||||
    }
 | 
			
		||||
    if (!this.settingsSet) {
 | 
			
		||||
      this.settingsSet = true;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user