From 34304250869b9d2b07c6c6b8f7b54dae5a7713a5 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 12 Dec 2022 16:40:43 +0100 Subject: [PATCH 1/5] fixed device bulk import with empty credentials --- .../server/service/device/DeviceBulkImportService.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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); } From d2233e51578b0b4eaba9594db1c4dc966105dd83 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 12 Dec 2022 18:40:50 +0100 Subject: [PATCH 2/5] added bulk import device test --- .../controller/BaseDeviceControllerTest.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) 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..a8456206f7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -55,6 +55,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; @@ -1235,6 +1238,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); From 7cb9166677d6c9f2efdddefbb5c394c9be82a183 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 13 Dec 2022 10:58:34 +0100 Subject: [PATCH 3/5] fixed saveDeviceWithCredentials notifications --- .../device/DefaultTbDeviceService.java | 9 ++-- .../controller/BaseDeviceControllerTest.java | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) 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/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index b97b346bd9..22edb56eab 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; @@ -180,6 +181,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(); From 5645527a3b6bf18c65952eaaee193f812b88436f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 13 Dec 2022 15:20:04 +0200 Subject: [PATCH 4/5] Update TBEL version to 1.0.4 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a6f2d114f161d4b4ad94bf3393826faade5c350e Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 13 Dec 2022 16:28:30 +0200 Subject: [PATCH 5/5] UI navigation improvements --- .../entity/entities-table.component.html | 3 ++- .../entity/entities-table-config.models.ts | 15 +++++++++++ .../dashboards-table-config.resolver.ts | 27 +++++++++++++------ .../rulechains-table-config.resolver.ts | 24 ++++++++++++----- .../widget/widget-library.component.html | 3 ++- .../widget/widget-library.component.scss | 8 ++++++ .../pages/widget/widget-library.component.ts | 1 + .../widgets-bundles-table-config.resolver.ts | 23 +++++++++++----- ui-ngx/src/styles.scss | 3 +++ 9 files changed, 85 insertions(+), 22 deletions(-) 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) {