From 09438ca9317611c02f75bcc2b2d76ae3347f46da Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 11 Mar 2026 17:10:48 +0200 Subject: [PATCH] Refactor IoT Hub update flow with checksum-based modification detection - Refactor update/delete endpoints to use installed item's own ID instead of marketplace itemId - Add SHA-256 checksum comparison to detect local entity modifications before update - Support force update to skip checksum check when user confirms overwrite - Add per-type update methods (widget, dashboard, calculated field, rule chain) - Add entityId field to CalculatedFieldInstalledItemDescriptor - Replace checkForUpdates with getItemsPublishedVersions API - Replace getInstalledItemInfos with getInstalledItemIds returning List - Remove unused IotHubInstalledItemInfo class and resolver - Set metadata version from saved rule chain in install/update flows - Add entity-modified confirmation dialog in update UI --- .../main/data/upgrade/basic/schema_update.sql | 3 +- .../server/controller/IotHubController.java | 37 ++- .../service/iot_hub/DefaultIotHubService.java | 235 ++++++++++++++++-- .../server/service/iot_hub/IotHubService.java | 10 +- .../iot_hub/UpdateItemVersionResult.java | 9 +- ...alculatedFieldInstalledItemDescriptor.java | 2 + .../data/iot_hub/IotHubInstalledItemInfo.java | 32 --- .../dao/iot_hub/IotHubInstalledItemDao.java | 8 +- .../iot_hub/IotHubInstalledItemService.java | 9 +- .../IotHubInstalledItemServiceImpl.java | 15 +- .../IotHubInstalledItemRepository.java | 14 +- .../iot_hub/JpaIotHubInstalledItemDao.java | 16 +- .../main/resources/sql/schema-entities.sql | 3 +- .../src/app/core/http/iot-hub-api.service.ts | 41 ++- .../iot-hub/iot-hub-browse.component.html | 5 +- .../pages/iot-hub/iot-hub-browse.component.ts | 67 +---- .../iot-hub-install-dialog.component.ts | 36 ++- .../iot-hub-installed-item-infos.resolver.ts | 25 -- .../iot-hub-installed-items.component.html | 10 +- .../iot-hub-installed-items.component.scss | 7 +- .../iot-hub-installed-items.component.ts | 69 ++--- .../iot-hub/iot-hub-item-card.component.html | 10 +- .../iot-hub/iot-hub-item-card.component.scss | 27 -- .../iot-hub/iot-hub-item-card.component.ts | 14 -- .../iot-hub-item-detail-dialog.component.html | 11 +- .../iot-hub-item-detail-dialog.component.ts | 54 ++-- .../pages/iot-hub/iot-hub-routing.module.ts | 7 - .../iot-hub-update-dialog.component.ts | 230 +++++++++++++++++ .../entity/entity-autocomplete.component.ts | 12 + .../entity/entity-select.component.html | 1 + .../entity/entity-select.component.ts | 27 +- .../iot-hub/iot-hub-installed-item.models.ts | 14 +- .../assets/locale/locale.constant-en_US.json | 3 + 33 files changed, 674 insertions(+), 389 deletions(-) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemInfo.java delete mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-item-infos.resolver.ts create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.ts diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 3df3837944..0e00ba0833 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -56,8 +56,7 @@ CREATE TABLE IF NOT EXISTS iot_hub_installed_item ( item_name VARCHAR NOT NULL, item_type VARCHAR NOT NULL, version VARCHAR NOT NULL, - descriptor JSONB NOT NULL, - CONSTRAINT iot_hub_installed_item_item_id_unq_key UNIQUE (tenant_id, item_id) + descriptor JSONB NOT NULL ); -- IOT HUB INSTALLED ITEM END \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java index 4e4b2a86e2..2568911c90 100644 --- a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java +++ b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java @@ -19,22 +19,24 @@ import io.swagger.v3.oas.annotations.Hidden; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; +import com.fasterxml.jackson.databind.JsonNode; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; -import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import java.util.List; import java.util.UUID; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.iot_hub.InstallItemVersionResult; @@ -55,16 +57,18 @@ public class IotHubController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/versions/{versionId}/install") @ResponseBody - public InstallItemVersionResult installItemVersion(@PathVariable String versionId) throws ThingsboardException { - return iotHubService.installItemVersion(getCurrentUser(), versionId); + public InstallItemVersionResult installItemVersion(@PathVariable String versionId, + @RequestBody(required = false) JsonNode data) throws ThingsboardException { + return iotHubService.installItemVersion(getCurrentUser(), versionId, data); } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @PostMapping("/installedItems/{itemId}/update/{versionId}") + @PostMapping("/installedItems/{installedItemId}/update/{versionId}") @ResponseBody - public UpdateItemVersionResult updateItemVersion(@PathVariable UUID itemId, - @PathVariable String versionId) throws ThingsboardException { - return iotHubService.updateItemVersion(getCurrentUser(), itemId, versionId); + public UpdateItemVersionResult updateItemVersion(@PathVariable UUID installedItemId, + @PathVariable String versionId, + @RequestParam(required = false, defaultValue = "false") boolean force) throws ThingsboardException { + return iotHubService.updateItemVersion(getCurrentUser(), new IotHubInstalledItemId(installedItemId), versionId, force); } @PreAuthorize("hasAuthority('TENANT_ADMIN')") @@ -80,23 +84,16 @@ public class IotHubController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/installedItems/byItemId/{itemId}") + @GetMapping("/installedItems/itemIds") @ResponseBody - public IotHubInstalledItem getInstalledItemByItemId(@PathVariable UUID itemId) throws ThingsboardException { - return iotHubInstalledItemService.findByTenantIdAndItemId(getTenantId(), itemId).orElse(null); + public List getInstalledItemIds() throws ThingsboardException { + return iotHubInstalledItemService.findInstalledItemIdsByTenantId(getTenantId()); } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/installedItems/info") + @DeleteMapping("/installedItems/{installedItemId}") @ResponseBody - public List getInstalledItemInfos() throws ThingsboardException { - return iotHubInstalledItemService.findInstalledItemInfosByTenantId(getTenantId()); - } - - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @DeleteMapping("/installedItems/{itemId}") - @ResponseBody - public void deleteInstalledItem(@PathVariable UUID itemId) throws ThingsboardException { - iotHubService.deleteInstalledItem(getCurrentUser(), itemId); + public void deleteInstalledItem(@PathVariable UUID installedItemId) throws ThingsboardException { + iotHubService.deleteInstalledItem(getCurrentUser(), new IotHubInstalledItemId(installedItemId)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java index 6a80412a99..af5e966c13 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -23,6 +23,8 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.iot_hub.CalculatedFieldInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.DashboardInstalledItemDescriptor; @@ -32,9 +34,9 @@ import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.RuleChainInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.WidgetInstalledItemDescriptor; import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.common.data.rule.RuleChainData; -import org.thingsboard.server.common.data.rule.RuleChainImportResult; +import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -50,6 +52,9 @@ import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.security.model.SecurityUser; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.HexFormat; import java.util.List; import java.util.UUID; @@ -73,7 +78,7 @@ public class DefaultIotHubService implements IotHubService { private final DeviceProfileService deviceProfileService; @Override - public InstallItemVersionResult installItemVersion(SecurityUser user, String versionId) { + public InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data) { TenantId tenantId = user.getTenantId(); log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId); @@ -85,17 +90,13 @@ public class DefaultIotHubService implements IotHubService { String version = versionInfo.get("version").asText(); log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType); - if (iotHubInstalledItemService.findByTenantIdAndItemId(tenantId, itemId).isPresent()) { - return InstallItemVersionResult.error("Item '" + itemName + "' is already installed"); - } - byte[] fileData = iotHubRestClient.getVersionFileData(versionId); log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0); IotHubInstalledItemDescriptor descriptor = switch (itemType) { case "WIDGET" -> installWidget(user, tenantId, fileData); case "DASHBOARD" -> installDashboard(user, tenantId, fileData); - case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData); + case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data); case "RULE_CHAIN" -> installRuleChain(tenantId, fileData); case "DEVICE" -> installDeviceProfile(user, tenantId, fileData); default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType); @@ -130,7 +131,7 @@ public class DefaultIotHubService implements IotHubService { } widgetTypeDetails.setId(null); widgetTypeDetails.setTenantId(tenantId); - WidgetTypeDetails saved = tbWidgetTypeService.save(widgetTypeDetails, true, user); + WidgetTypeDetails saved = tbWidgetTypeService.save(widgetTypeDetails, false, user); log.debug("[{}] Widget installed: {}", tenantId, saved.getName()); WidgetInstalledItemDescriptor descriptor = new WidgetInstalledItemDescriptor(); descriptor.setWidgetTypeId(saved.getId()); @@ -153,7 +154,7 @@ public class DefaultIotHubService implements IotHubService { return descriptor; } - private CalculatedFieldInstalledItemDescriptor installCalculatedField(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { + private CalculatedFieldInstalledItemDescriptor installCalculatedField(SecurityUser user, TenantId tenantId, byte[] fileData, JsonNode data) throws Exception { CalculatedField calculatedField; try { calculatedField = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); @@ -162,10 +163,15 @@ public class DefaultIotHubService implements IotHubService { } calculatedField.setId(null); calculatedField.setTenantId(tenantId); + if (data != null && data.has("entityId")) { + EntityId entityId = JacksonUtil.treeToValue(data.get("entityId"), EntityId.class); + calculatedField.setEntityId(entityId); + } CalculatedField saved = tbCalculatedFieldService.save(calculatedField, user); log.debug("[{}] Calculated field installed: {}", tenantId, saved.getName()); CalculatedFieldInstalledItemDescriptor descriptor = new CalculatedFieldInstalledItemDescriptor(); descriptor.setCalculatedFieldId(saved.getId()); + descriptor.setEntityId(saved.getEntityId()); return descriptor; } @@ -191,6 +197,7 @@ public class DefaultIotHubService implements IotHubService { RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); metadata.setRuleChainId(savedRuleChain.getId()); + metadata.setVersion(savedRuleChain.getVersion()); ruleChainService.saveRuleChainMetaData(tenantId, metadata, tbRuleChainService::updateRuleNodeConfiguration); log.debug("[{}] Rule chain installed: {}", tenantId, savedRuleChain.getName()); @@ -214,18 +221,30 @@ public class DefaultIotHubService implements IotHubService { } @Override - public UpdateItemVersionResult updateItemVersion(SecurityUser user, UUID itemId, String versionId) { + public UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force) { TenantId tenantId = user.getTenantId(); - log.info("[{}] Updating IoT Hub item {} to version: {}", tenantId, itemId, versionId); + log.info("[{}] Updating IoT Hub installed item {} to version: {}", tenantId, installedItemId, versionId); try { - IotHubInstalledItem installedItem = iotHubInstalledItemService.findByTenantIdAndItemId(tenantId, itemId) - .orElseThrow(() -> new IllegalArgumentException("Installed item not found")); + IotHubInstalledItem installedItem = iotHubInstalledItemService.findById(tenantId, installedItemId); + if (installedItem == null) { + throw new IllegalArgumentException("Installed item not found"); + } JsonNode installedVersionInfo = iotHubRestClient.getVersionInfo(installedItem.getItemVersionId().toString()); String installedChecksum = installedVersionInfo.has("checksum") ? installedVersionInfo.get("checksum").asText() : null; log.info("[{}] Installed version info: name={}, version={}, checksum={}", tenantId, installedItem.getItemName(), installedItem.getVersion(), installedChecksum); + if (!force) { + String entityChecksum = calculateEntityChecksum(tenantId, installedItem); + boolean entityModified = installedChecksum != null && !installedChecksum.equals(entityChecksum); + log.info("[{}] Entity checksum: {}, modified: {}", tenantId, entityChecksum, entityModified); + + if (entityModified) { + return UpdateItemVersionResult.entityModified(); + } + } + JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); String itemName = versionInfo.get("name").asText(); String version = versionInfo.get("version").asText(); @@ -233,22 +252,198 @@ public class DefaultIotHubService implements IotHubService { byte[] fileData = iotHubRestClient.getVersionFileData(versionId); log.info("[{}] Fetched update file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0); - // TODO: apply update per item type + IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor(); + String itemType = installedItem.getItemType(); + switch (itemType) { + case "WIDGET" -> updateWidget(user, tenantId, (WidgetInstalledItemDescriptor) descriptor, fileData); + case "DASHBOARD" -> updateDashboard(user, tenantId, (DashboardInstalledItemDescriptor) descriptor, fileData); + case "CALCULATED_FIELD" -> updateCalculatedField(user, tenantId, (CalculatedFieldInstalledItemDescriptor) descriptor, fileData); + case "RULE_CHAIN" -> updateRuleChain(tenantId, (RuleChainInstalledItemDescriptor) descriptor, fileData); + case "DEVICE" -> updateDeviceProfile(user, tenantId, fileData); + default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType); + } + + installedItem.setItemVersionId(UUID.fromString(versionId)); + installedItem.setItemName(itemName); + installedItem.setVersion(version); + iotHubInstalledItemService.save(tenantId, installedItem); log.info("[{}] Successfully updated IoT Hub item {} to version: {}", tenantId, itemName, version); return UpdateItemVersionResult.success(installedItem.getDescriptor()); } catch (Exception e) { - log.error("[{}] Failed to update IoT Hub item {} to version: {}", tenantId, itemId, versionId, e); + log.error("[{}] Failed to update IoT Hub installed item {} to version: {}", tenantId, installedItemId, versionId, e); return UpdateItemVersionResult.error(e.getMessage()); } } + private void updateWidget(SecurityUser user, TenantId tenantId, WidgetInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { + WidgetTypeDetails newWidgetType; + try { + newWidgetType = JacksonUtil.fromString(new String(fileData), WidgetTypeDetails.class, true); + } catch (Exception e) { + throw new Exception("Failed to parse widget data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + } + WidgetTypeDetails existing = widgetTypeService.findWidgetTypeDetailsById(tenantId, descriptor.getWidgetTypeId()); + if (existing == null) { + throw new Exception("Widget not found for update"); + } + existing.setName(newWidgetType.getName()); + existing.setDescriptor(newWidgetType.getDescriptor()); + tbWidgetTypeService.save(existing, false, user); + } + + private void updateDashboard(SecurityUser user, TenantId tenantId, DashboardInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { + Dashboard newDashboard; + try { + newDashboard = JacksonUtil.fromString(new String(fileData), Dashboard.class, true); + } catch (Exception e) { + throw new Exception("Failed to parse dashboard data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + } + Dashboard existing = dashboardService.findDashboardById(tenantId, descriptor.getDashboardId()); + if (existing == null) { + throw new Exception("Dashboard not found for update"); + } + existing.setTitle(newDashboard.getTitle()); + existing.setConfiguration(newDashboard.getConfiguration()); + tbDashboardService.save(existing, user); + } + + private void updateCalculatedField(SecurityUser user, TenantId tenantId, CalculatedFieldInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { + CalculatedField newCf; + try { + newCf = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); + } catch (Exception e) { + throw new Exception("Failed to parse calculated field data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + } + CalculatedField existing = calculatedFieldService.findById(tenantId, descriptor.getCalculatedFieldId()); + if (existing == null) { + throw new Exception("Calculated field not found for update"); + } + existing.setName(newCf.getName()); + existing.setType(newCf.getType()); + existing.setConfiguration(newCf.getConfiguration()); + tbCalculatedFieldService.save(existing, user); + } + + private void updateRuleChain(TenantId tenantId, RuleChainInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { + JsonNode json = JacksonUtil.toJsonNode(new String(fileData)); + RuleChainMetaData metadata; + try { + metadata = JacksonUtil.fromString(json.get("metadata").toString(), RuleChainMetaData.class, true); + } catch (Exception e) { + throw new Exception("Failed to parse rule chain metadata: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + } + RuleChain existing = ruleChainService.findRuleChainById(tenantId, descriptor.getRuleChainId()); + if (existing == null) { + throw new Exception("Rule chain not found for update"); + } + RuleChain newRuleChain; + try { + newRuleChain = JacksonUtil.fromString(json.get("ruleChain").toString(), RuleChain.class, true); + } catch (Exception e) { + throw new Exception("Failed to parse rule chain: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + } + existing.setName(newRuleChain.getName()); + RuleChain savedRuleChain = ruleChainService.saveRuleChain(existing); + metadata.setRuleChainId(savedRuleChain.getId()); + metadata.setVersion(savedRuleChain.getVersion()); + ruleChainService.saveRuleChainMetaData(tenantId, metadata, tbRuleChainService::updateRuleNodeConfiguration); + } + + private void updateDeviceProfile(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { + // TODO: implement device profile update + } + + private String calculateEntityChecksum(TenantId tenantId, IotHubInstalledItem installedItem) { + IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor(); + if (descriptor instanceof WidgetInstalledItemDescriptor wd) { + return calculateWidgetChecksum(tenantId, wd); + } else if (descriptor instanceof DashboardInstalledItemDescriptor dd) { + return calculateDashboardChecksum(tenantId, dd); + } else if (descriptor instanceof CalculatedFieldInstalledItemDescriptor cd) { + return calculateCalculatedFieldChecksum(tenantId, cd); + } else if (descriptor instanceof RuleChainInstalledItemDescriptor rd) { + return calculateRuleChainChecksum(tenantId, rd); + } + return null; + } + + private String calculateCalculatedFieldChecksum(TenantId tenantId, CalculatedFieldInstalledItemDescriptor descriptor) { + CalculatedField cf = calculatedFieldService.findById(tenantId, descriptor.getCalculatedFieldId()); + if (cf == null) { + return null; + } + String content = (cf.getName() != null ? cf.getName() : "") + + (cf.getType() != null ? cf.getType().name() : "") + + (cf.getConfiguration() != null ? JacksonUtil.valueToTree(cf.getConfiguration()).toString() : ""); + return sha256(content); + } + + private String calculateDashboardChecksum(TenantId tenantId, DashboardInstalledItemDescriptor descriptor) { + Dashboard dashboard = dashboardService.findDashboardById(tenantId, descriptor.getDashboardId()); + if (dashboard == null) { + return null; + } + String content = (dashboard.getTitle() != null ? dashboard.getTitle() : "") + + (dashboard.getConfiguration() != null ? dashboard.getConfiguration().toString() : ""); + return sha256(content); + } + + private String calculateWidgetChecksum(TenantId tenantId, WidgetInstalledItemDescriptor descriptor) { + WidgetTypeDetails widgetType = widgetTypeService.findWidgetTypeDetailsById(tenantId, descriptor.getWidgetTypeId()); + if (widgetType == null) { + return null; + } + String content = (widgetType.getFqn() != null ? widgetType.getFqn() : "") + + (widgetType.getName() != null ? widgetType.getName() : "") + + (widgetType.getDescriptor() != null ? widgetType.getDescriptor().toString() : ""); + return sha256(content); + } + + private String calculateRuleChainChecksum(TenantId tenantId, RuleChainInstalledItemDescriptor descriptor) { + RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, descriptor.getRuleChainId()); + if (ruleChain == null) { + return null; + } + RuleChainMetaData metadata = ruleChainService.loadRuleChainMetaData(tenantId, descriptor.getRuleChainId()); + StringBuilder content = new StringBuilder(); + content.append(ruleChain.getName() != null ? ruleChain.getName() : ""); + content.append(ruleChain.getType() != null ? ruleChain.getType().name() : ""); + if (metadata.getNodes() != null) { + for (RuleNode node : metadata.getNodes()) { + content.append(node.getType() != null ? node.getType() : ""); + content.append(node.getName() != null ? node.getName() : ""); + content.append(node.getConfiguration() != null ? node.getConfiguration().toString() : ""); + } + } + if (metadata.getConnections() != null) { + for (NodeConnectionInfo conn : metadata.getConnections()) { + content.append(conn.getFromIndex()); + content.append(conn.getToIndex()); + content.append(conn.getType() != null ? conn.getType() : ""); + } + } + return sha256(content.toString()); + } + + private static String sha256(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash); + } catch (Exception e) { + throw new RuntimeException("Failed to calculate SHA-256", e); + } + } + @Override - public void deleteInstalledItem(SecurityUser user, UUID itemId) { + public void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId) { TenantId tenantId = user.getTenantId(); - IotHubInstalledItem installedItem = iotHubInstalledItemService.findByTenantIdAndItemId(tenantId, itemId) - .orElseThrow(() -> new IllegalArgumentException("Installed item not found")); + IotHubInstalledItem installedItem = iotHubInstalledItemService.findById(tenantId, installedItemId); + if (installedItem == null) { + throw new IllegalArgumentException("Installed item not found"); + } IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor(); if (descriptor instanceof WidgetInstalledItemDescriptor wd) { @@ -277,7 +472,7 @@ public class DefaultIotHubService implements IotHubService { // no entity to delete for now } - iotHubInstalledItemService.deleteByTenantIdAndItemId(tenantId, itemId); + iotHubInstalledItemService.deleteById(tenantId, installedItemId); log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java index e17ca5d4f2..c7faf14bae 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java @@ -15,15 +15,15 @@ */ package org.thingsboard.server.service.iot_hub; +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.UUID; - public interface IotHubService { - InstallItemVersionResult installItemVersion(SecurityUser user, String versionId); + InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data); - UpdateItemVersionResult updateItemVersion(SecurityUser user, UUID itemId, String versionId); + UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force); - void deleteInstalledItem(SecurityUser user, UUID itemId); + void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId); } diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/UpdateItemVersionResult.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/UpdateItemVersionResult.java index b056b29e6a..1ba7b0a7b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/UpdateItemVersionResult.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/UpdateItemVersionResult.java @@ -26,15 +26,20 @@ import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; public class UpdateItemVersionResult { private boolean success; + private boolean entityModified; private String errorMessage; private IotHubInstalledItemDescriptor descriptor; public static UpdateItemVersionResult success(IotHubInstalledItemDescriptor descriptor) { - return new UpdateItemVersionResult(true, null, descriptor); + return new UpdateItemVersionResult(true, false, null, descriptor); + } + + public static UpdateItemVersionResult entityModified() { + return new UpdateItemVersionResult(false, true, null, null); } public static UpdateItemVersionResult error(String errorMessage) { - return new UpdateItemVersionResult(false, errorMessage, null); + return new UpdateItemVersionResult(false, false, errorMessage, null); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java index b47b9f8cc8..ceedbe2dea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java @@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.iot_hub; import lombok.Data; import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; @Data public class CalculatedFieldInstalledItemDescriptor implements IotHubInstalledItemDescriptor { private CalculatedFieldId calculatedFieldId; + private EntityId entityId; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemInfo.java deleted file mode 100644 index 19eb74dccb..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemInfo.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2026 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. - */ -package org.thingsboard.server.common.data.iot_hub; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.UUID; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class IotHubInstalledItemInfo { - - private UUID itemId; - private UUID itemVersionId; - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java index 300b1cd620..82abff4bf1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java @@ -17,24 +17,18 @@ package org.thingsboard.server.dao.iot_hub; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; -import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; -import java.util.Optional; import java.util.UUID; public interface IotHubInstalledItemDao extends Dao { - Optional findByTenantIdAndItemId(TenantId tenantId, UUID itemId); - PageData findByTenantId(TenantId tenantId, PageLink pageLink); - List findInstalledItemInfosByTenantId(TenantId tenantId); - - boolean deleteByTenantIdAndItemId(TenantId tenantId, UUID itemId); + List findInstalledItemIdsByTenantId(TenantId tenantId); void deleteByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java index 1cee8f10a2..ccc1de3bc1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java @@ -15,27 +15,26 @@ */ package org.thingsboard.server.dao.iot_hub; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; -import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import java.util.List; -import java.util.Optional; import java.util.UUID; public interface IotHubInstalledItemService { IotHubInstalledItem save(TenantId tenantId, IotHubInstalledItem item); - Optional findByTenantIdAndItemId(TenantId tenantId, UUID itemId); + IotHubInstalledItem findById(TenantId tenantId, IotHubInstalledItemId id); PageData findByTenantId(TenantId tenantId, PageLink pageLink); - List findInstalledItemInfosByTenantId(TenantId tenantId); + List findInstalledItemIdsByTenantId(TenantId tenantId); - boolean deleteByTenantIdAndItemId(TenantId tenantId, UUID itemId); + void deleteById(TenantId tenantId, IotHubInstalledItemId id); void deleteByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java index 37593df30b..1642ba3870 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java @@ -18,14 +18,13 @@ package org.thingsboard.server.dao.iot_hub; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; -import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import java.util.List; -import java.util.Optional; import java.util.UUID; @Service @@ -42,8 +41,8 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService { } @Override - public Optional findByTenantIdAndItemId(TenantId tenantId, UUID itemId) { - return iotHubInstalledItemDao.findByTenantIdAndItemId(tenantId, itemId); + public IotHubInstalledItem findById(TenantId tenantId, IotHubInstalledItemId id) { + return iotHubInstalledItemDao.findById(tenantId, id.getId()); } @Override @@ -52,13 +51,13 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService { } @Override - public List findInstalledItemInfosByTenantId(TenantId tenantId) { - return iotHubInstalledItemDao.findInstalledItemInfosByTenantId(tenantId); + public List findInstalledItemIdsByTenantId(TenantId tenantId) { + return iotHubInstalledItemDao.findInstalledItemIdsByTenantId(tenantId); } @Override - public boolean deleteByTenantIdAndItemId(TenantId tenantId, UUID itemId) { - return iotHubInstalledItemDao.deleteByTenantIdAndItemId(tenantId, itemId); + public void deleteById(TenantId tenantId, IotHubInstalledItemId id) { + iotHubInstalledItemDao.removeById(tenantId, id.getId()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java index fa4b98da6d..bb245133b5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java @@ -22,20 +22,15 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemInfo; import org.thingsboard.server.dao.model.sql.IotHubInstalledItemEntity; import java.util.List; -import java.util.Optional; import java.util.UUID; interface IotHubInstalledItemRepository extends JpaRepository { - Optional findByTenantIdAndItemId(UUID tenantId, UUID itemId); - - @Query("SELECT new org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemInfo(item.itemId, item.itemVersionId) " + - "FROM IotHubInstalledItemEntity item WHERE item.tenantId = :tenantId") - List findInstalledItemInfosByTenantId(@Param("tenantId") UUID tenantId); + @Query("SELECT DISTINCT item.itemId FROM IotHubInstalledItemEntity item WHERE item.tenantId = :tenantId") + List findInstalledItemIdsByTenantId(@Param("tenantId") UUID tenantId); @Query(""" SELECT item FROM IotHubInstalledItemEntity item @@ -47,11 +42,6 @@ interface IotHubInstalledItemRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); - @Transactional - @Modifying - @Query("DELETE FROM IotHubInstalledItemEntity item WHERE item.tenantId = :tenantId AND item.itemId = :itemId") - int deleteByTenantIdAndItemId(@Param("tenantId") UUID tenantId, @Param("itemId") UUID itemId); - @Transactional @Modifying @Query(value = "DELETE FROM iot_hub_installed_item WHERE tenant_id = :tenantId", nativeQuery = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java index 265026d710..ee837f6363 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java @@ -23,7 +23,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; -import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; @@ -34,7 +33,6 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; -import java.util.Optional; import java.util.UUID; @SqlDao @@ -44,11 +42,6 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao findByTenantIdAndItemId(TenantId tenantId, UUID itemId) { - return repository.findByTenantIdAndItemId(tenantId.getId(), itemId).map(DaoUtil::getData); - } - @Override public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { return DaoUtil.toPageData(repository.findByTenantId( @@ -59,13 +52,8 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao 0; - } - - @Override - public List findInstalledItemInfosByTenantId(TenantId tenantId) { - return repository.findInstalledItemInfosByTenantId(tenantId.getId()); + public List findInstalledItemIdsByTenantId(TenantId tenantId) { + return repository.findInstalledItemIdsByTenantId(tenantId.getId()); } @Override diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index bd2a1c1c7b..67aa39fd8e 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -986,6 +986,5 @@ CREATE TABLE IF NOT EXISTS iot_hub_installed_item ( item_name VARCHAR NOT NULL, item_type VARCHAR NOT NULL, version VARCHAR NOT NULL, - descriptor JSONB NOT NULL, - CONSTRAINT iot_hub_installed_item_item_id_unq_key UNIQUE (tenant_id, item_id) + descriptor JSONB NOT NULL ); diff --git a/ui-ngx/src/app/core/http/iot-hub-api.service.ts b/ui-ngx/src/app/core/http/iot-hub-api.service.ts index 587e37a155..4d4d375e5f 100644 --- a/ui-ngx/src/app/core/http/iot-hub-api.service.ts +++ b/ui-ngx/src/app/core/http/iot-hub-api.service.ts @@ -22,7 +22,7 @@ import { PageData } from '@shared/models/page/page-data'; import { PageLink } from '@shared/models/page/page-link'; import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { CreatorView } from '@shared/models/iot-hub/iot-hub-creator.models'; -import { IotHubInstalledItem, IotHubInstalledItemInfo, InstallItemVersionResult, UpdateItemVersionResult, ItemUpdateInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; +import { IotHubInstalledItem, InstallItemVersionResult, UpdateItemVersionResult, ItemPublishedVersionInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params'; import { InterceptorConfig } from '@core/interceptors/interceptor-config'; import { AppState } from '@core/core.state'; @@ -112,32 +112,29 @@ export class IotHubApiService { ); } - public installItemVersion(versionId: string, config?: IotHubRequestConfig): Observable { + public installItemVersion(versionId: string, config?: IotHubRequestConfig, data?: any): Observable { return this.http.post( `/api/iot-hub/versions/${versionId}/install`, - null, + data || null, { params: this.buildParams(config) } ); } - public updateItemVersion(itemId: string, versionId: string, config?: IotHubRequestConfig): Observable { + public updateItemVersion(installedItemId: string, versionId: string, config?: IotHubRequestConfig, force?: boolean): Observable { + let params = this.buildParams(config); + if (force) { + params = params.set('force', 'true'); + } return this.http.post( - `/api/iot-hub/installedItems/${itemId}/update/${versionId}`, + `/api/iot-hub/installedItems/${installedItemId}/update/${versionId}`, null, - { params: this.buildParams(config) } - ); - } - - public getInstalledItemByItemId(itemId: string, config?: IotHubRequestConfig): Observable { - return this.http.get( - `/api/iot-hub/installedItems/byItemId/${itemId}`, - { params: this.buildParams(config) } + { params } ); } - public getInstalledItemInfos(config?: IotHubRequestConfig): Observable { - return this.http.get( - `/api/iot-hub/installedItems/info`, + public getInstalledItemIds(config?: IotHubRequestConfig): Observable { + return this.http.get( + `/api/iot-hub/installedItems/itemIds`, { params: this.buildParams(config) } ); } @@ -149,17 +146,17 @@ export class IotHubApiService { ); } - public deleteInstalledItem(itemId: string, config?: IotHubRequestConfig): Observable { + public deleteInstalledItem(installedItemId: string, config?: IotHubRequestConfig): Observable { return this.http.delete( - `/api/iot-hub/installedItems/${itemId}`, + `/api/iot-hub/installedItems/${installedItemId}`, { params: this.buildParams(config) } ); } - public checkForUpdates(infos: IotHubInstalledItemInfo[], config?: IotHubRequestConfig): Observable { - return this.http.post( - `${this.baseUrl}/api/versions/checkForUpdates`, - infos, + public getItemsPublishedVersions(itemIds: string[], config?: IotHubRequestConfig): Observable { + return this.http.post( + `${this.baseUrl}/api/versions/publishedVersions`, + itemIds, { params: this.buildParams(config) } ); } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html index 5b350ec9ee..3894da7685 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html @@ -193,12 +193,9 @@ [item]="item" [showTypeChip]="false" [showCreator]="!creatorId" - [installed]="isInstalled(item)" - [installedItemInfo]="installedItemsMap.get(item.itemId) || null" (cardClick)="openItemDetail($event)" (creatorClick)="navigateToCreator($event)" - (installClick)="installItem($event)" - (updateClick)="updateItem($event)"> + (installClick)="installItem($event)"> } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts index 27f370856d..a9b7243898 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts @@ -27,13 +27,11 @@ import { } from '@shared/models/iot-hub/iot-hub-item.models'; import { cfTypeTranslations, widgetTypeTranslations, ruleChainTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; -import { IotHubInstalledItemInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { MatDialog } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData } from './iot-hub-item-detail-dialog.component'; import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component'; -import { TbIotHubUpdateDialogComponent, IotHubUpdateDialogData } from './iot-hub-update-dialog.component'; interface SortOption { value: string; @@ -91,8 +89,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { widgetTypes: Map = widgetTypeTranslations; ruleChainTypes: Map = ruleChainTypeTranslations; - installedItemsMap = new Map(); - private searchSubject = new Subject(); private destroy$ = new Subject(); @@ -100,8 +96,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { private iotHubApiService: IotHubApiService, private dialog: MatDialog, private translate: TranslateService, - private router: Router, - private route: ActivatedRoute + private router: Router ) {} ngOnInit(): void { @@ -113,7 +108,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); }); this.updateCategories(); - this.initInstalledItemInfos(); this.loadItems(); } @@ -328,65 +322,27 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { } } - isInstalled(item: MpItemVersionView): boolean { - return this.installedItemsMap.has(item.itemId); - } - openItemDetail(item: MpItemVersionView): void { - const installedInfo = this.installedItemsMap.get(item.itemId); - const dialogRef = this.dialog.open(TbIotHubItemDetailDialogComponent, { + this.dialog.open(TbIotHubItemDetailDialogComponent, { panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], autoFocus: false, data: { item, - iotHubApiService: this.iotHubApiService, - installedItemInfo: installedInfo + iotHubApiService: this.iotHubApiService } as IotHubItemDetailDialogData }); - dialogRef.afterClosed().subscribe(result => { - if (result === 'installed' || result === 'updated') { - this.loadInstalledItemInfos(); - } - }); } installItem(item: MpItemVersionView): void { - const dialogRef = this.dialog.open(TbIotHubInstallDialogComponent, { + this.dialog.open(TbIotHubInstallDialogComponent, { panelClass: ['tb-dialog'], data: { item, iotHubApiService: this.iotHubApiService } as IotHubInstallDialogData }); - dialogRef.afterClosed().subscribe(result => { - if (result === 'installed') { - this.loadInstalledItemInfos(); - } - }); } - updateItem(item: MpItemVersionView): void { - const installedInfo = this.installedItemsMap.get(item.itemId); - if (!installedInfo) { - return; - } - const dialogRef = this.dialog.open(TbIotHubUpdateDialogComponent, { - panelClass: ['tb-dialog'], - data: { - itemId: item.itemId, - itemName: item.name, - itemType: item.type, - version: item.version, - versionId: item.id, - iotHubApiService: this.iotHubApiService - } as IotHubUpdateDialogData - }); - dialogRef.afterClosed().subscribe(result => { - if (result === 'updated') { - this.loadInstalledItemInfos(); - } - }); - } navigateToCreator(creatorId: string): void { this.router.navigate(['/iot-hub/creator', creatorId]); @@ -396,19 +352,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.router.navigate(['/iot-hub/installed']); } - private initInstalledItemInfos(): void { - const infos: IotHubInstalledItemInfo[] = this.route.snapshot.data['installedItemInfos'] || []; - this.installedItemsMap.clear(); - infos.forEach(info => this.installedItemsMap.set(info.itemId, info)); - } - - private loadInstalledItemInfos(): void { - this.iotHubApiService.getInstalledItemInfos({ignoreLoading: true}).subscribe(infos => { - this.installedItemsMap.clear(); - infos.forEach(info => this.installedItemsMap.set(info.itemId, info)); - }); - } - private updateCategories(): void { this.categories = getCategoriesForType(this.activeType); } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts index 4c7edea820..fd48b0f999 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts @@ -23,6 +23,7 @@ import { IotHubInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-in import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; +import { EntityId } from '@shared/models/id/entity-id'; import { getEntityDetailsPageURL } from '@core/utils'; export interface IotHubInstallDialogData { @@ -30,7 +31,7 @@ export interface IotHubInstallDialogData { iotHubApiService: IotHubApiService; } -export type InstallState = 'confirm' | 'installing' | 'success' | 'error'; +export type InstallState = 'select-entity' | 'confirm' | 'installing' | 'success' | 'error'; @Component({ selector: 'tb-iot-hub-install-dialog', @@ -49,6 +50,24 @@ export type InstallState = 'confirm' | 'installing' | 'success' | 'error'; } + @case ('select-entity') { +

{{ 'iot-hub.install-item-title' | translate }}

+ +

{{ 'iot-hub.select-entity-for-cf' | translate:{ name: item.name } }}

+ + +
+ + + + + } @case ('installing') {

{{ 'iot-hub.install-item-title' | translate }}

@@ -147,6 +166,10 @@ export class TbIotHubInstallDialogComponent { errorMessage = ''; entityDetailsUrl: string | null = null; + selectedEntityId: EntityId | null = null; + cfEntityTypes: EntityType[] = [EntityType.DEVICE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE]; + defaultCfEntityType = EntityType.DEVICE_PROFILE; + constructor( @Inject(MAT_DIALOG_DATA) public data: IotHubInstallDialogData, private dialogRef: MatDialogRef, @@ -162,9 +185,18 @@ export class TbIotHubInstallDialogComponent { } install(): void { + if (this.item.type === ItemType.CALCULATED_FIELD) { + this.state = 'select-entity'; + return; + } + this.doInstall(); + } + + doInstall(): void { this.state = 'installing'; const versionId = this.item.id as string; - this.data.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }).subscribe({ + const data = this.selectedEntityId ? { entityId: this.selectedEntityId } : undefined; + this.data.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }, data).subscribe({ next: (result) => { if (result.success) { this.state = 'success'; diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-item-infos.resolver.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-item-infos.resolver.ts deleted file mode 100644 index f54eaebf0a..0000000000 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-item-infos.resolver.ts +++ /dev/null @@ -1,25 +0,0 @@ -/// -/// Copyright © 2016-2026 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. -/// - -import { inject } from '@angular/core'; -import { ResolveFn } from '@angular/router'; -import { IotHubApiService } from '@core/http/iot-hub-api.service'; -import { IotHubInstalledItemInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; - -export const iotHubInstalledItemInfosResolver: ResolveFn = () => { - const iotHubApiService = inject(IotHubApiService); - return iotHubApiService.getInstalledItemInfos({ ignoreLoading: true }); -}; diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html index 24f2e9f9e0..193a9f8d63 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html @@ -83,17 +83,17 @@ {{ updatesChecked ? ('iot-hub.updates' | translate) : '' }} @if (updatesChecked) { - @if (getUpdateInfo(item); as updateInfo) { - @if (updateInfo.hasUpdate) { + @if (getPublishedVersionInfo(item); as publishedInfo) { + @if (publishedInfo.publishedVersionId !== item.itemVersionId) { } @else { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss index 7e8f080553..d066356922 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss @@ -186,7 +186,12 @@ width: 100%; } - .mat-column-itemType, + .mat-column-itemType { + width: 150px; + min-width: 150px; + max-width: 150px; + } + .mat-column-version { width: 100px; min-width: 100px; diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts index 4dc173637e..317ba59816 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts @@ -15,7 +15,7 @@ /// import { Component, OnInit, AfterViewInit, ViewChild } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { MatSort } from '@angular/material/sort'; import { MatPaginator } from '@angular/material/paginator'; @@ -26,10 +26,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { Subject } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; +import { debounceTime, switchMap } from 'rxjs/operators'; import { PageLink } from '@shared/models/page/page-link'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; -import { IotHubInstalledItem, IotHubInstalledItemInfo, ItemUpdateInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; +import { IotHubInstalledItem, ItemPublishedVersionInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { EntityType } from '@shared/models/entity-type.models'; import { getEntityDetailsPageURL } from '@core/utils'; @@ -52,8 +52,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { isLoading = false; textSearch = ''; - installedItemInfos: IotHubInstalledItemInfo[] = []; - updateInfoMap = new Map(); + publishedVersionMap = new Map(); updatesChecked = false; isCheckingUpdates = false; @@ -62,26 +61,16 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { @ViewChild(MatSort, {static: true}) sort: MatSort; @ViewChild(MatPaginator, {static: true}) paginator: MatPaginator; - private static readonly ITEM_TYPE_TO_ENTITY_TYPE: Record = { - 'WIDGET': EntityType.WIDGET_TYPE, - 'DASHBOARD': EntityType.DASHBOARD, - 'CALCULATED_FIELD': EntityType.CALCULATED_FIELD, - 'RULE_CHAIN': EntityType.RULE_CHAIN, - 'DEVICE': EntityType.DEVICE_PROFILE - }; - constructor( private iotHubApiService: IotHubApiService, private dialogService: DialogService, private translate: TranslateService, private store: Store, private router: Router, - private dialog: MatDialog, - private route: ActivatedRoute + private dialog: MatDialog ) {} ngOnInit(): void { - this.installedItemInfos = this.route.snapshot.data['installedItemInfos'] || []; this.searchSubject.pipe( debounceTime(300) ).subscribe(() => { @@ -123,7 +112,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { this.translate.instant('action.yes') ).subscribe(result => { if (result) { - this.iotHubApiService.deleteInstalledItem(item.itemId).subscribe({ + this.iotHubApiService.deleteInstalledItem(item.id.id).subscribe({ next: () => { this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('iot-hub.installed-item-deleted', {name: item.itemName}), @@ -167,7 +156,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { data: { item: versionView, iotHubApiService: this.iotHubApiService, - installedDescriptor: item.descriptor + installedItem: item } as IotHubItemDetailDialogData }); }); @@ -178,14 +167,25 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { switch (descriptor.type) { case 'WIDGET': return descriptor.widgetTypeId?.id; case 'DASHBOARD': return descriptor.dashboardId?.id; - case 'CALCULATED_FIELD': return descriptor.calculatedFieldId?.id; + case 'CALCULATED_FIELD': return descriptor.entityId?.id; case 'RULE_CHAIN': return descriptor.ruleChainId?.id; default: return null; } } + getEntityType(item: IotHubInstalledItem): EntityType | null { + const descriptor = item.descriptor; + switch (descriptor.type) { + case 'WIDGET': return EntityType.WIDGET_TYPE; + case 'DASHBOARD': return EntityType.DASHBOARD; + case 'CALCULATED_FIELD': return descriptor.entityId?.entityType as EntityType; + case 'RULE_CHAIN': return EntityType.RULE_CHAIN; + default: return null; + } + } + openEntity(item: IotHubInstalledItem): void { - const entityType = TbIotHubInstalledItemsComponent.ITEM_TYPE_TO_ENTITY_TYPE[item.itemType]; + const entityType = this.getEntityType(item); const entityId = this.getEntityId(item); if (entityType && entityId) { const url = getEntityDetailsPageURL(entityId, entityType); @@ -197,10 +197,12 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { checkForUpdates(): void { this.isCheckingUpdates = true; - this.iotHubApiService.checkForUpdates(this.installedItemInfos, { ignoreLoading: true }).subscribe({ - next: (updates) => { - this.updateInfoMap.clear(); - updates.forEach(info => this.updateInfoMap.set(info.itemId, info)); + this.iotHubApiService.getInstalledItemIds({ ignoreLoading: true }).pipe( + switchMap(itemIds => this.iotHubApiService.getItemsPublishedVersions(itemIds, { ignoreLoading: true })) + ).subscribe({ + next: (infos) => { + this.publishedVersionMap.clear(); + infos.forEach(info => this.publishedVersionMap.set(info.itemId, info)); this.updatesChecked = true; this.isCheckingUpdates = false; }, @@ -210,29 +212,28 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { }); } - viewUpdateDetails(updateInfo: ItemUpdateInfo, installedItem: IotHubInstalledItem): void { - this.iotHubApiService.getVersionInfo(updateInfo.latestItemVersionId, {ignoreLoading: true}).subscribe(versionView => { + viewUpdateDetails(publishedInfo: ItemPublishedVersionInfo, installedItem: IotHubInstalledItem): void { + this.iotHubApiService.getVersionInfo(publishedInfo.publishedVersionId, {ignoreLoading: true}).subscribe(versionView => { this.dialog.open(TbIotHubItemDetailDialogComponent, { panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { item: versionView, iotHubApiService: this.iotHubApiService, - installedDescriptor: installedItem.descriptor, - installedItemInfo: { itemId: installedItem.itemId, itemVersionId: installedItem.itemVersionId } + installedItem } as IotHubItemDetailDialogData }); }); } - updateItem(item: IotHubInstalledItem, updateInfo: ItemUpdateInfo): void { + updateItem(item: IotHubInstalledItem, publishedInfo: ItemPublishedVersionInfo): void { const dialogRef = this.dialog.open(TbIotHubUpdateDialogComponent, { panelClass: ['tb-dialog'], data: { - itemId: item.itemId, + installedItemId: item.id.id, itemName: item.itemName, itemType: item.itemType as ItemType, - version: updateInfo.latestVersion, - versionId: updateInfo.latestItemVersionId, + version: publishedInfo.publishedVersion, + versionId: publishedInfo.publishedVersionId, iotHubApiService: this.iotHubApiService } as IotHubUpdateDialogData }); @@ -243,8 +244,8 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { }); } - getUpdateInfo(item: IotHubInstalledItem): ItemUpdateInfo | undefined { - return this.updateInfoMap.get(item.itemId); + getPublishedVersionInfo(item: IotHubInstalledItem): ItemPublishedVersionInfo | undefined { + return this.publishedVersionMap.get(item.itemId); } private loadData(): void { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html index 510d626afd..1750548a2a 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html @@ -100,15 +100,7 @@ download {{ item.totalInstallCount | shortNumber }} - @if (installed) { - @if (hasUpdate()) { - - } @else { - {{ 'iot-hub.installed' | translate }} - } - } @else { - - } + diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss index 8eed3e6980..afe2357bb3 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss @@ -349,31 +349,4 @@ } } - .tb-iot-hub-card-installed-label { - padding: 5px 14px; - border-radius: 6px; - background: #e8f5e9; - color: #2e7d32; - font-size: 12px; - font-weight: 600; - white-space: nowrap; - } - - .tb-iot-hub-card-update-btn { - padding: 5px 14px; - border-radius: 6px; - border: 1px solid #e65100; - background: transparent; - color: #e65100; - font-size: 12px; - font-weight: 600; - cursor: pointer; - white-space: nowrap; - transition: all 0.15s; - - &:hover { - background: #e65100; - color: #fff; - } - } } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts index 41a664b49c..87f5502696 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts @@ -19,7 +19,6 @@ import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTransl import { ItemType, itemTypeTranslations, getCategoriesForType, useCaseTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { TranslateService } from '@ngx-translate/core'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; -import { IotHubInstalledItemInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; @Component({ selector: 'tb-iot-hub-item-card', @@ -36,12 +35,9 @@ export class TbIotHubItemCardComponent { @Input() item: MpItemVersionView; @Input() showCreator = true; @Input() showTypeChip = true; - @Input() installed = false; - @Input() installedItemInfo: IotHubInstalledItemInfo | null = null; @Output() cardClick = new EventEmitter(); @Output() creatorClick = new EventEmitter(); @Output() installClick = new EventEmitter(); - @Output() updateClick = new EventEmitter(); typeTranslations = itemTypeTranslations; @@ -190,21 +186,11 @@ export class TbIotHubItemCardComponent { this.cardClick.emit(this.item); } - hasUpdate(): boolean { - return this.installed && this.installedItemInfo != null - && this.installedItemInfo.itemVersionId !== this.item.id; - } - onInstallClick(event: MouseEvent): void { event.stopPropagation(); this.installClick.emit(this.item); } - onUpdateClick(event: MouseEvent): void { - event.stopPropagation(); - this.updateClick.emit(this.item); - } - onCreatorClick(event: MouseEvent): void { event.stopPropagation(); this.creatorClick.emit(this.item.creatorId); diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html index c4400af2cc..770a6bdf15 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html @@ -175,20 +175,19 @@