diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000000..01ba7d863e --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,88 @@ +# +# Copyright © 2016-2022 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +changelog: + exclude: + labels: + - Ignore for release + categories: + - title: 'Major Core & Rule Engine' + labels: + - 'Major Core' + - 'Major Rule Engine' + exclude: + labels: + - 'Bug' + - title: 'Major UI' + labels: + - 'Major UI' + exclude: + labels: + - 'Bug' + - title: 'Major Transport' + labels: + - 'Major Transport' + exclude: + labels: + - 'Bug' + - title: 'Major Edge' + labels: + - 'Major Edge' + exclude: + labels: + - 'Bug' + - title: 'Core & Rule Engine' + labels: + - 'Core' + - 'Rule Engine' + exclude: + labels: + - 'Bug' + - title: 'UI' + labels: + - 'UI' + exclude: + labels: + - 'Bug' + - title: 'Transport' + labels: + - 'Transport' + exclude: + labels: + - 'Bug' + - title: 'Edge' + labels: + - 'Edge' + exclude: + labels: + - 'Bug' + - title: 'Bug: Core & Rule Engine' + labels: + - 'Core' + - 'Rule Engine' + - 'Bug' + - title: 'Bug: UI' + labels: + - 'UI' + - 'Bug' + - title: 'Bug: Transport' + labels: + - 'Transport' + - 'Bug' + - title: 'Bug: Edge' + labels: + - 'Edge' + - 'Bug' diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index a02ae3ac4d..90b5921c71 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -51,6 +51,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; @@ -59,6 +60,7 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; @@ -177,6 +179,14 @@ public class ActorSystemContext { @Getter private DeviceService deviceService; + @Autowired + @Getter + private DeviceProfileService deviceProfileService; + + @Autowired + @Getter + private AssetProfileService assetProfileService; + @Autowired @Getter private DeviceCredentialsService deviceCredentialsService; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index ebe2a8424a..de50179e44 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -72,12 +72,14 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; @@ -560,6 +562,16 @@ class DefaultTbContext implements TbContext { return mainCtx.getDeviceService(); } + @Override + public DeviceProfileService getDeviceProfileService() { + return mainCtx.getDeviceProfileService(); + } + + @Override + public AssetProfileService getAssetProfileService() { + return mainCtx.getAssetProfileService(); + } + @Override public DeviceCredentialsService getDeviceCredentialsService() { return mainCtx.getDeviceCredentialsService(); diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 87bf110c81..2e2b1a1204 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -29,10 +29,12 @@ import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.adapter.NativeWebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.config.WebSocketConfiguration; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -52,6 +54,8 @@ import javax.websocket.Session; import java.io.IOException; import java.net.URI; import java.security.InvalidParameterException; +import java.util.Optional; +import java.util.Queue; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -305,22 +309,24 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (internalId != null) { SessionMetaData sessionMd = internalSessionMap.get(internalId); if (sessionMd != null) { - var tenantProfileConfiguration = tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration(); - if (StringUtils.isNotEmpty(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())) { - TbRateLimits rateLimits = perSessionUpdateLimits.computeIfAbsent(sessionRef.getSessionId(), sid -> new TbRateLimits(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())); - if (!rateLimits.tryConsume()) { - if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) { - log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached" - , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId); - sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}"); + var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); + if (tenantProfileConfiguration != null) { + if (StringUtils.isNotEmpty(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())) { + TbRateLimits rateLimits = perSessionUpdateLimits.computeIfAbsent(sessionRef.getSessionId(), sid -> new TbRateLimits(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())); + if (!rateLimits.tryConsume()) { + if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) { + log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached" + , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId); + sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}"); + } + return; + } else { + log.debug("[{}][{}][{}] Session is no longer blacklisted.", sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId); + blacklistedSessions.remove(externalId); } - return; } else { - log.debug("[{}][{}][{}] Session is no longer blacklisted.", sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId); - blacklistedSessions.remove(externalId); + perSessionUpdateLimits.remove(sessionRef.getSessionId()); } - } else { - perSessionUpdateLimits.remove(sessionRef.getSessionId()); } sessionMd.sendMsg(msg); } else { @@ -365,8 +371,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke } private boolean checkLimits(WebSocketSession session, WebSocketSessionRef sessionRef) throws Exception { - var tenantProfileConfiguration = - tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration(); + var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); if (tenantProfileConfiguration == null) { return true; } @@ -433,7 +438,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke } private void cleanupLimits(WebSocketSession session, WebSocketSessionRef sessionRef) { - var tenantProfileConfiguration = tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration(); + var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); + if (tenantProfileConfiguration == null) return; String sessionId = session.getId(); perSessionUpdateLimits.remove(sessionRef.getSessionId()); @@ -466,4 +472,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke } } + private DefaultTenantProfileConfiguration getTenantProfileConfiguration(TelemetryWebSocketSessionRef sessionRef) { + return Optional.ofNullable(tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId())) + .map(TenantProfile::getDefaultProfileConfiguration).orElse(null); + } + } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 7337497194..9f8d58b566 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfilePr import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration; import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; @@ -104,7 +105,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { protected Device saveEntity(SecurityUser user, Device entity, Map fields) { DeviceCredentials deviceCredentials; try { - deviceCredentials = createDeviceCredentials(fields); + deviceCredentials = createDeviceCredentials(entity.getTenantId(), entity.getId(), fields); deviceCredentialsService.formatCredentials(deviceCredentials); } catch (Exception e) { throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage()); @@ -136,7 +137,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { } @SneakyThrows - private DeviceCredentials createDeviceCredentials(Map fields) { + private DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceId deviceId, Map fields) { DeviceCredentials credentials = new DeviceCredentials(); if (fields.containsKey(BulkImportColumnType.LWM2M_CLIENT_ENDPOINT)) { credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); @@ -147,7 +148,9 @@ public class DeviceBulkImportService extends AbstractBulkImportService { } else if (CollectionUtils.containsAny(fields.keySet(), EnumSet.of(BulkImportColumnType.MQTT_CLIENT_ID, BulkImportColumnType.MQTT_USER_NAME, BulkImportColumnType.MQTT_PASSWORD))) { credentials.setCredentialsType(DeviceCredentialsType.MQTT_BASIC); setUpBasicMqttCredentials(fields, credentials); - } else { + } else if (deviceId != null && !fields.containsKey(BulkImportColumnType.ACCESS_TOKEN)) { + credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, deviceId); + } else { credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); setUpAccessTokenCredentials(fields, credentials); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java index f72092bedf..25053676fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java @@ -76,17 +76,18 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T @Override public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, User user) throws ThingsboardException { - ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + boolean isCreate = device.getId() == null; + ActionType actionType = isCreate ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = device.getTenantId(); try { + Device oldDevice = isCreate ? null : deviceService.findDeviceById(tenantId, device.getId()); Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials)); notificationEntityService.notifyCreateOrUpdateDevice(tenantId, savedDevice.getId(), savedDevice.getCustomerId(), - savedDevice, device, actionType, user); + savedDevice, oldDevice, actionType, user); return savedDevice; } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE), device, - actionType, user, e); + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE), device, actionType, user, e); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index bb0693669a..2ccbdfadc9 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -563,13 +563,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc for (ReadTsKvQueryResult queryResult : queryResults) { String queryKey = queriesKeys.get(queryResult.getQueryId()); if (queryKey != null) { - TsValue[] tsValues = entityData.getTimeseries().get(queryKey); - if (tsValues == null) { - tsValues = queryResult.toTsValues(); - } else { - tsValues = ArrayUtils.addAll(tsValues, queryResult.toTsValues()); - } - entityData.getTimeseries().put(queryKey, tsValues); + entityData.getTimeseries().merge(queryKey, queryResult.toTsValues(), ArrayUtils::addAll); + lastTsMap.merge(queryKey, queryResult.getLastEntryTs(), Math::max); } else { log.warn("ReadTsKvQueryResult for {} {} has queryId not matching the initial query", entityData.getEntityId().getEntityType(), entityData.getEntityId()); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index 37d3cd2e71..d78403a884 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -87,7 +87,7 @@ public abstract class AbstractBulkImportService WsCmdHandler newCmdHandler(java.util.function.Function cmdExtractor, BiConsumer handler) { return new WsCmdHandler<>(cmdExtractor, handler); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index b97b346bd9..0e9d840711 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; @@ -55,6 +56,9 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType; +import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; +import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; @@ -180,6 +184,58 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(foundDevice.getName(), savedDevice.getName()); } + @Test + public void testSaveDeviceWithCredentials() throws Exception { + String testToken = "TEST_TOKEN"; + + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + deviceCredentials.setCredentialsId(testToken); + + SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(device, deviceCredentials); + + Mockito.reset(tbClusterService, auditLogService, gatewayNotificationsService); + + Device savedDevice = readResponse(doPost("/api/device-with-credentials", saveRequest).andExpect(status().isOk()), Device.class); + + Device oldDevice = new Device(savedDevice); + + testNotifyEntityOneTimeMsgToEdgeServiceNever(savedDevice, savedDevice.getId(), savedDevice.getId(), + savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.ADDED); + testNotificationUpdateGatewayNever(); + + Assert.assertNotNull(savedDevice); + Assert.assertNotNull(savedDevice.getId()); + Assert.assertTrue(savedDevice.getCreatedTime() > 0); + Assert.assertEquals(savedTenant.getId(), savedDevice.getTenantId()); + Assert.assertNotNull(savedDevice.getCustomerId()); + Assert.assertEquals(NULL_UUID, savedDevice.getCustomerId().getId()); + Assert.assertEquals(device.getName(), savedDevice.getName()); + + DeviceCredentials foundDeviceCredentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + Assert.assertNotNull(foundDeviceCredentials); + Assert.assertNotNull(foundDeviceCredentials.getId()); + Assert.assertEquals(savedDevice.getId(), foundDeviceCredentials.getDeviceId()); + Assert.assertEquals(DeviceCredentialsType.ACCESS_TOKEN, foundDeviceCredentials.getCredentialsType()); + Assert.assertEquals(testToken, foundDeviceCredentials.getCredentialsId()); + + Mockito.reset(tbClusterService, auditLogService, gatewayNotificationsService); + + savedDevice.setName("My new device"); + doPost("/api/device", savedDevice, Device.class); + + testNotifyEntityAllOneTime(savedDevice, savedDevice.getId(), savedDevice.getId(), savedTenant.getId(), + tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UPDATED); + testNotificationUpdateGatewayOneTime(savedDevice, oldDevice); + } + @Test public void saveDeviceWithViolationOfValidation() throws Exception { Device device = new Device(); @@ -1235,6 +1291,66 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { testEntityDaoWithRelationsTransactionalException(deviceDao, savedTenant.getId(), deviceId, "/api/device/" + deviceId); } + @Test + public void testBulkImportDeviceWithoutCredentials() throws Exception { + String deviceName = "some_device"; + String deviceType = "some_type"; + BulkImportRequest request = new BulkImportRequest(); + request.setFile(String.format("NAME,TYPE\n%s,%s", deviceName, deviceType)); + BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping(); + BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping(); + name.setType(BulkImportColumnType.NAME); + BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping(); + type.setType(BulkImportColumnType.TYPE); + List columns = new ArrayList<>(); + columns.add(name); + columns.add(type); + + mapping.setColumns(columns); + mapping.setDelimiter(','); + mapping.setUpdate(true); + mapping.setHeader(true); + request.setMapping(mapping); + + BulkImportResult deviceBulkImportResult = doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {}); + + Assert.assertEquals(1, deviceBulkImportResult.getCreated().get()); + Assert.assertEquals(0, deviceBulkImportResult.getErrors().get()); + Assert.assertEquals(0, deviceBulkImportResult.getUpdated().get()); + Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty()); + + Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); + + Assert.assertNotNull(savedDevice); + Assert.assertEquals(deviceName, savedDevice.getName()); + Assert.assertEquals(deviceType, savedDevice.getType()); + + DeviceCredentials savedCredentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + Assert.assertNotNull(savedCredentials); + Assert.assertNotNull(savedCredentials.getId()); + Assert.assertEquals(savedDevice.getId(), savedCredentials.getDeviceId()); + Assert.assertEquals(DeviceCredentialsType.ACCESS_TOKEN, savedCredentials.getCredentialsType()); + Assert.assertNotNull(savedCredentials.getCredentialsId()); + Assert.assertEquals(20, savedCredentials.getCredentialsId().length()); + + deviceBulkImportResult = doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {}); + + Assert.assertEquals(0, deviceBulkImportResult.getCreated().get()); + Assert.assertEquals(0, deviceBulkImportResult.getErrors().get()); + Assert.assertEquals(1, deviceBulkImportResult.getUpdated().get()); + Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty()); + + Device updatedDevice = doGet("/api/device/" + savedDevice.getId().getId(), Device.class); + Assert.assertEquals(savedDevice, updatedDevice); + + DeviceCredentials updatedCredentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + Assert.assertEquals(savedCredentials, updatedCredentials); + } + private Device createDevice(String name) { Device device = new Device(); device.setName(name); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java index ba229b68a5..d12d956a2f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FutureCallback; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; @@ -23,11 +24,15 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.DeviceTypeFilter; @@ -41,12 +46,14 @@ import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; import java.util.Arrays; import java.util.Collections; @@ -54,6 +61,8 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j @@ -78,6 +87,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { @After public void tearDown() throws Exception { + loginTenantAdmin(); doDelete("/api/device/" + device.getId().getId()) .andExpect(status().isOk()); } @@ -532,6 +542,28 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { Assert.assertEquals(new TsValue(dataPoint5.getLastUpdateTs(), dataPoint5.getValueAsString()), attrValue); } + @Test + public void testAttributesSubscription_sysAdmin() throws Exception { + loginSysAdmin(); + SingleEntityFilter entityFilter = new SingleEntityFilter(); + entityFilter.setSingleEntity(tenantId); + + assertThatNoException().isThrownBy(() -> { + JsonNode update = getWsClient().subscribeForAttributes(tenantId, TbAttributeSubscriptionScope.SERVER_SCOPE.name(), List.of("attr")); + assertThat(update.get("errorMsg").isNull()).isTrue(); + assertThat(update.get("errorCode").asInt()).isEqualTo(SubscriptionErrorCode.NO_ERROR.getCode()); + }); + + getWsClient().registerWaitForUpdate(); + String expectedAttrValue = "42"; + sendAttributes(TenantId.SYS_TENANT_ID, tenantId, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of( + new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry("attr", expectedAttrValue)) + )); + JsonNode update = JacksonUtil.toJsonNode(getWsClient().waitForUpdate()); + assertThat(update).isNotNull(); + assertThat(update.get("data").get("attr").get(0).get(1).asText()).isEqualTo(expectedAttrValue); + } + private void sendTelemetry(Device device, List tsData) throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); tsService.saveAndNotify(device.getTenantId(), null, device.getId(), tsData, 0, new FutureCallback() { @@ -549,8 +581,12 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { } private void sendAttributes(Device device, TbAttributeSubscriptionScope scope, List attrData) throws InterruptedException { + sendAttributes(device.getTenantId(), device.getId(), scope, attrData); + } + + private void sendAttributes(TenantId tenantId, EntityId entityId, TbAttributeSubscriptionScope scope, List attrData) throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); - tsService.saveAndNotify(device.getTenantId(), device.getId(), scope.name(), attrData, new FutureCallback() { + tsService.saveAndNotify(tenantId, entityId, scope.name(), attrData, new FutureCallback() { @Override public void onSuccess(@Nullable Void result) { latch.countDown(); diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index aca00222a3..3de71789e9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -16,16 +16,20 @@ package org.thingsboard.server.controller; import lombok.Getter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryPluginCmdsWrapper; +import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; @@ -204,6 +208,21 @@ public class TbTestWebSocketClient extends WebSocketClient { return parseDataReply(waitForReply()); } + public JsonNode subscribeForAttributes(EntityId entityId, String scope, List keys) { + AttributesSubscriptionCmd cmd = new AttributesSubscriptionCmd(); + cmd.setCmdId(1); + cmd.setEntityType(entityId.getEntityType().toString()); + cmd.setEntityId(entityId.getId().toString()); + cmd.setScope(scope); + cmd.setKeys(String.join(",", keys)); + TelemetryPluginCmdsWrapper cmdsWrapper = new TelemetryPluginCmdsWrapper(); + cmdsWrapper.setAttrSubCmds(List.of(cmd)); + JsonNode msg = JacksonUtil.valueToTree(cmdsWrapper); + ((ObjectNode) msg.get("attrSubCmds").get(0)).remove("type"); + send(msg.toString()); + return JacksonUtil.toJsonNode(waitForReply()); + } + public EntityDataUpdate sendHistoryCmd(List keys, long startTs, long timeWindow) { return sendHistoryCmd(keys, startTs, timeWindow, (EntityDataQuery) null); } diff --git a/pom.xml b/pom.xml index f1c3adbad8..3d51b748ce 100755 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ 3.5.5 3.21.9 1.42.1 - 1.0.3 + 1.0.4 1.18.18 1.2.4 4.1.75.Final diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index e23d850ead..205692c9a9 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -42,12 +42,14 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; @@ -223,6 +225,10 @@ public interface TbContext { DeviceService getDeviceService(); + DeviceProfileService getDeviceProfileService(); + + AssetProfileService getAssetProfileService(); + DeviceCredentialsService getDeviceCredentialsService(); TbClusterService getClusterService(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java index 2dc27b6f92..d018b86922 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java @@ -40,7 +40,7 @@ import java.util.concurrent.ExecutionException; name = "split array msg", configClazz = EmptyNodeConfiguration.class, nodeDescription = "Split array message into several msgs", - nodeDetails = "Split the array fetched from the msg body. If the msg data is not a JSON object returns the " + nodeDetails = "Split the array fetched from the msg body. If the msg data is not a JSON array returns the " + "incoming message as outbound message with Failure chain, otherwise returns " + "inner objects of the extracted array as separate messages via Success chain.", uiResources = {"static/rulenode/rulenode-core-config.js"}, @@ -61,7 +61,9 @@ public class TbSplitArrayMsgNode implements TbNode { JsonNode jsonNode = JacksonUtil.toJsonNode(msg.getData()); if (jsonNode.isArray()) { ArrayNode data = (ArrayNode) jsonNode; - if (data.size() == 1) { + if (data.isEmpty()) { + ctx.ack(msg); + } else if (data.size() == 1) { ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(data.get(0)))); } else { TbMsgCallbackWrapper wrapper = new MultipleTbMsgsCallbackWrapper(data.size(), new TbMsgCallback() { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index 5f023b2efb..ed26721661 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -251,7 +251,8 @@ , P extends PageLink = P entitiesDeleteEnabled = true; detailsPanelEnabled = true; hideDetailsTabsOnEdit = true; + rowPointer = false; actionsColumnTitle = null; entityTranslations: EntityTypeTranslation; entityResources: EntityTypeResource; @@ -220,6 +221,20 @@ export class EntityTableConfig, P extends PageLink = P } } + toggleEntityDetails($event: Event, entity: T) { + if (this.table) { + this.table.toggleEntityDetails($event, entity); + } + } + + isDetailsOpen(): boolean { + if (this.table) { + return this.table.isDetailsOpen; + } else { + return false; + } + } + getActivatedRoute(): ActivatedRoute { if (this.table) { return this.table.route; diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts index fddc9e94ce..ea22fa590a 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts @@ -94,6 +94,8 @@ export class DashboardsTableConfigResolver implements Resolve this.translate.instant('dashboard.delete-dashboard-title', {dashboardTitle: dashboard.title}); this.config.deleteEntityContent = () => this.translate.instant('dashboard.delete-dashboard-text'); @@ -107,6 +109,15 @@ export class DashboardsTableConfigResolver implements Resolve this.onDashboardAction(action); this.config.detailsReadonly = () => (this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user'); + + this.config.handleRowClick = ($event, dashboard) => { + if (this.config.isDetailsOpen()) { + this.config.toggleEntityDetails($event, dashboard); + } else { + this.openDashboard($event, dashboard); + } + return true; + }; } resolve(route: ActivatedRouteSnapshot): Observable> { @@ -197,14 +208,6 @@ export class DashboardsTableConfigResolver implements Resolve> { const actions: Array> = []; - actions.push( - { - name: this.translate.instant('dashboard.open-dashboard'), - icon: 'dashboard', - isEnabled: () => true, - onAction: ($event, entity) => this.openDashboard($event, entity) - } - ); if (dashboardScope === 'tenant') { actions.push( { @@ -271,6 +274,14 @@ export class DashboardsTableConfigResolver implements Resolve true, + onAction: ($event, entity) => this.config.toggleEntityDetails($event, entity) + } + ); return actions; } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts index a3ff812359..b414f2ff21 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts @@ -74,6 +74,8 @@ export class RuleChainsTableConfigResolver implements Resolve this.translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name}); this.config.deleteEntityContent = () => this.translate.instant('rulechain.delete-rulechain-text'); @@ -83,6 +85,14 @@ export class RuleChainsTableConfigResolver implements Resolve this.saveRuleChain(ruleChain); this.config.deleteEntity = id => this.ruleChainService.deleteRuleChain(id.id); this.config.onEntityAction = action => this.onRuleChainAction(action); + this.config.handleRowClick = ($event, ruleChain) => { + if (this.config.isDetailsOpen()) { + this.config.toggleEntityDetails($event, ruleChain); + } else { + this.openRuleChain($event, ruleChain); + } + return true; + }; } resolve(route: ActivatedRouteSnapshot): EntityTableConfig { @@ -214,12 +224,6 @@ export class RuleChainsTableConfigResolver implements Resolve> { const actions: Array> = []; actions.push( - { - name: this.translate.instant('rulechain.open-rulechain'), - icon: 'settings_ethernet', - isEnabled: () => true, - onAction: ($event, entity) => this.openRuleChain($event, entity) - }, { name: this.translate.instant('rulechain.export'), icon: 'file_download', @@ -275,6 +279,14 @@ export class RuleChainsTableConfigResolver implements Resolve true, + onAction: ($event, entity) => this.config.toggleEntityDetails($event, entity) + } + ); return actions; } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html index 5ce4f3f1cf..4c74e68f62 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html @@ -27,7 +27,8 @@ style="display: flex;" class="mat-headline tb-absolute-fill">widgets-bundle.empty - widgetsBundle ? widgetsBundle.title : ''; @@ -89,17 +91,17 @@ export class WidgetsBundlesTableConfigResolver implements Resolve true, - onAction: ($event, entity) => this.openWidgetsBundle($event, entity) - }, { name: this.translate.instant('widgets-bundle.export'), icon: 'file_download', isEnabled: () => true, onAction: ($event, entity) => this.exportWidgetsBundle($event, entity) + }, + { + name: this.translate.instant('widgets-bundle.widgets-bundle-details'), + icon: 'edit', + isEnabled: () => true, + onAction: ($event, entity) => this.config.toggleEntityDetails($event, entity) } ); @@ -114,6 +116,15 @@ export class WidgetsBundlesTableConfigResolver implements Resolve this.widgetsService.saveWidgetsBundle(widgetsBundle); this.config.deleteEntity = id => this.widgetsService.deleteWidgetsBundle(id.id); this.config.onEntityAction = action => this.onWidgetsBundleAction(action); + + this.config.handleRowClick = ($event, widgetsBundle) => { + if (this.config.isDetailsOpen()) { + this.config.toggleEntityDetails($event, widgetsBundle); + } else { + this.openWidgetsBundle($event, widgetsBundle); + } + return true; + }; } resolve(): EntityTableConfig { diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 8c6fffabf1..44480b8e1b 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -1011,6 +1011,9 @@ mat-label { &.tb-current-entity { background-color: #e9e9e9; } + &.tb-pointer { + cursor: pointer; + } } .mat-row:not(.mat-row-select), .mat-header-row:not(.mat-row-select) {