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,30 +359,34 @@ 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(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,6 +342,7 @@ public class DefaultTbClusterService implements TbClusterService {
|
||||
|
||||
@Override
|
||||
public void onResourceChange(TbResourceInfo resource, TbQueueCallback 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()
|
||||
@ -349,20 +352,23 @@ public class DefaultTbClusterService implements TbClusterService {
|
||||
.setResourceKey(resource.getResourceKey())
|
||||
.build();
|
||||
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
|
||||
broadcast(transportMsg, callback);
|
||||
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()
|
||||
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(resourceUpdateMsg).build();
|
||||
broadcast(transportMsg, callback);
|
||||
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,9 +181,12 @@ public class CalculateDeltaNode implements TbNode {
|
||||
|
||||
private ListenableFuture<ValueWithTs> getLatestFromCacheOrFetchFromDb(TbContext ctx, TbMsg msg) {
|
||||
EntityId originator = msg.getOriginator();
|
||||
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