Browse Source

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<UUID>
- 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
pull/15347/head
Igor Kulikov 3 months ago
parent
commit
09438ca931
  1. 3
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 37
      application/src/main/java/org/thingsboard/server/controller/IotHubController.java
  3. 235
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  4. 10
      application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java
  5. 9
      application/src/main/java/org/thingsboard/server/service/iot_hub/UpdateItemVersionResult.java
  6. 2
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java
  7. 32
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemInfo.java
  8. 8
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java
  9. 9
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java
  10. 15
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java
  11. 14
      dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java
  12. 16
      dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java
  13. 3
      dao/src/main/resources/sql/schema-entities.sql
  14. 41
      ui-ngx/src/app/core/http/iot-hub-api.service.ts
  15. 5
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html
  16. 67
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts
  17. 36
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts
  18. 25
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-item-infos.resolver.ts
  19. 10
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html
  20. 7
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss
  21. 69
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts
  22. 10
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html
  23. 27
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss
  24. 14
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts
  25. 11
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html
  26. 54
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts
  27. 7
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts
  28. 230
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.ts
  29. 12
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  30. 1
      ui-ngx/src/app/shared/components/entity/entity-select.component.html
  31. 27
      ui-ngx/src/app/shared/components/entity/entity-select.component.ts
  32. 14
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts
  33. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json

3
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

37
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<UUID> getInstalledItemIds() throws ThingsboardException {
return iotHubInstalledItemService.findInstalledItemIdsByTenantId(getTenantId());
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/installedItems/info")
@DeleteMapping("/installedItems/{installedItemId}")
@ResponseBody
public List<IotHubInstalledItemInfo> 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));
}
}

235
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());
}
}

10
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);
}

9
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);
}
}

2
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;
}

32
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemInfo.java

@ -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;
}

8
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<IotHubInstalledItem> {
Optional<IotHubInstalledItem> findByTenantIdAndItemId(TenantId tenantId, UUID itemId);
PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, PageLink pageLink);
List<IotHubInstalledItemInfo> findInstalledItemInfosByTenantId(TenantId tenantId);
boolean deleteByTenantIdAndItemId(TenantId tenantId, UUID itemId);
List<UUID> findInstalledItemIdsByTenantId(TenantId tenantId);
void deleteByTenantId(TenantId tenantId);

9
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<IotHubInstalledItem> findByTenantIdAndItemId(TenantId tenantId, UUID itemId);
IotHubInstalledItem findById(TenantId tenantId, IotHubInstalledItemId id);
PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, PageLink pageLink);
List<IotHubInstalledItemInfo> findInstalledItemInfosByTenantId(TenantId tenantId);
List<UUID> findInstalledItemIdsByTenantId(TenantId tenantId);
boolean deleteByTenantIdAndItemId(TenantId tenantId, UUID itemId);
void deleteById(TenantId tenantId, IotHubInstalledItemId id);
void deleteByTenantId(TenantId tenantId);

15
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<IotHubInstalledItem> 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<IotHubInstalledItemInfo> findInstalledItemInfosByTenantId(TenantId tenantId) {
return iotHubInstalledItemDao.findInstalledItemInfosByTenantId(tenantId);
public List<UUID> 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

14
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<IotHubInstalledItemEntity, UUID> {
Optional<IotHubInstalledItemEntity> 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<IotHubInstalledItemInfo> findInstalledItemInfosByTenantId(@Param("tenantId") UUID tenantId);
@Query("SELECT DISTINCT item.itemId FROM IotHubInstalledItemEntity item WHERE item.tenantId = :tenantId")
List<UUID> findInstalledItemIdsByTenantId(@Param("tenantId") UUID tenantId);
@Query("""
SELECT item FROM IotHubInstalledItemEntity item
@ -47,11 +42,6 @@ interface IotHubInstalledItemRepository extends JpaRepository<IotHubInstalledIte
""")
Page<IotHubInstalledItemEntity> 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)

16
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<IotHubInstalledItemEntity
private final IotHubInstalledItemRepository repository;
@Override
public Optional<IotHubInstalledItem> findByTenantIdAndItemId(TenantId tenantId, UUID itemId) {
return repository.findByTenantIdAndItemId(tenantId.getId(), itemId).map(DaoUtil::getData);
}
@Override
public PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(repository.findByTenantId(
@ -59,13 +52,8 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao<IotHubInstalledItemEntity
}
@Override
public boolean deleteByTenantIdAndItemId(TenantId tenantId, UUID itemId) {
return repository.deleteByTenantIdAndItemId(tenantId.getId(), itemId) > 0;
}
@Override
public List<IotHubInstalledItemInfo> findInstalledItemInfosByTenantId(TenantId tenantId) {
return repository.findInstalledItemInfosByTenantId(tenantId.getId());
public List<UUID> findInstalledItemIdsByTenantId(TenantId tenantId) {
return repository.findInstalledItemIdsByTenantId(tenantId.getId());
}
@Override

3
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
);

41
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<InstallItemVersionResult> {
public installItemVersion(versionId: string, config?: IotHubRequestConfig, data?: any): Observable<InstallItemVersionResult> {
return this.http.post<InstallItemVersionResult>(
`/api/iot-hub/versions/${versionId}/install`,
null,
data || null,
{ params: this.buildParams(config) }
);
}
public updateItemVersion(itemId: string, versionId: string, config?: IotHubRequestConfig): Observable<UpdateItemVersionResult> {
public updateItemVersion(installedItemId: string, versionId: string, config?: IotHubRequestConfig, force?: boolean): Observable<UpdateItemVersionResult> {
let params = this.buildParams(config);
if (force) {
params = params.set('force', 'true');
}
return this.http.post<UpdateItemVersionResult>(
`/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<IotHubInstalledItem> {
return this.http.get<IotHubInstalledItem>(
`/api/iot-hub/installedItems/byItemId/${itemId}`,
{ params: this.buildParams(config) }
{ params }
);
}
public getInstalledItemInfos(config?: IotHubRequestConfig): Observable<IotHubInstalledItemInfo[]> {
return this.http.get<IotHubInstalledItemInfo[]>(
`/api/iot-hub/installedItems/info`,
public getInstalledItemIds(config?: IotHubRequestConfig): Observable<string[]> {
return this.http.get<string[]>(
`/api/iot-hub/installedItems/itemIds`,
{ params: this.buildParams(config) }
);
}
@ -149,17 +146,17 @@ export class IotHubApiService {
);
}
public deleteInstalledItem(itemId: string, config?: IotHubRequestConfig): Observable<void> {
public deleteInstalledItem(installedItemId: string, config?: IotHubRequestConfig): Observable<void> {
return this.http.delete<void>(
`/api/iot-hub/installedItems/${itemId}`,
`/api/iot-hub/installedItems/${installedItemId}`,
{ params: this.buildParams(config) }
);
}
public checkForUpdates(infos: IotHubInstalledItemInfo[], config?: IotHubRequestConfig): Observable<ItemUpdateInfo[]> {
return this.http.post<ItemUpdateInfo[]>(
`${this.baseUrl}/api/versions/checkForUpdates`,
infos,
public getItemsPublishedVersions(itemIds: string[], config?: IotHubRequestConfig): Observable<ItemPublishedVersionInfo[]> {
return this.http.post<ItemPublishedVersionInfo[]>(
`${this.baseUrl}/api/versions/publishedVersions`,
itemIds,
{ params: this.buildParams(config) }
);
}

5
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)">
</tb-iot-hub-item-card>
}
</div>

67
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<string, string> = widgetTypeTranslations;
ruleChainTypes: Map<string, string> = ruleChainTypeTranslations;
installedItemsMap = new Map<string, IotHubInstalledItemInfo>();
private searchSubject = new Subject<string>();
private destroy$ = new Subject<void>();
@ -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);
}

36
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';
<button mat-raised-button color="primary" (click)="install()">{{ 'iot-hub.install' | translate }}</button>
</mat-dialog-actions>
}
@case ('select-entity') {
<h2 mat-dialog-title>{{ 'iot-hub.install-item-title' | translate }}</h2>
<mat-dialog-content>
<p>{{ 'iot-hub.select-entity-for-cf' | translate:{ name: item.name } }}</p>
<tb-entity-select
[(ngModel)]="selectedEntityId"
[allowedEntityTypes]="cfEntityTypes"
[defaultEntityType]="defaultCfEntityType"
[filterAllowedEntityTypes]="false"
appearance="outline"
required>
</tb-entity-select>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-raised-button color="primary" [disabled]="!selectedEntityId" (click)="doInstall()">{{ 'iot-hub.install' | translate }}</button>
</mat-dialog-actions>
}
@case ('installing') {
<h2 mat-dialog-title>{{ 'iot-hub.install-item-title' | translate }}</h2>
<mat-dialog-content>
@ -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<TbIotHubInstallDialogComponent>,
@ -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';

25
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-item-infos.resolver.ts

@ -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<IotHubInstalledItemInfo[]> = () => {
const iotHubApiService = inject(IotHubApiService);
return iotHubApiService.getInstalledItemInfos({ ignoreLoading: true });
};

10
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html

@ -83,17 +83,17 @@
<th mat-header-cell *matHeaderCellDef>{{ updatesChecked ? ('iot-hub.updates' | translate) : '' }}</th>
<td mat-cell *matCellDef="let item">
@if (updatesChecked) {
@if (getUpdateInfo(item); as updateInfo) {
@if (updateInfo.hasUpdate) {
@if (getPublishedVersionInfo(item); as publishedInfo) {
@if (publishedInfo.publishedVersionId !== item.itemVersionId) {
<button mat-stroked-button class="tb-iot-hub-update-button"
[matTooltip]="'iot-hub.view-item-details' | translate"
matTooltipPosition="above"
(click)="viewUpdateDetails(updateInfo, item)">
(click)="viewUpdateDetails(publishedInfo, item)">
<mat-icon>info_outline</mat-icon>
{{ updateInfo.latestVersion }}
{{ publishedInfo.publishedVersion }}
</button>
<button mat-stroked-button color="primary" class="tb-iot-hub-update-button"
(click)="updateItem(item, updateInfo)">
(click)="updateItem(item, publishedInfo)">
{{ 'iot-hub.update' | translate }}
</button>
} @else {

7
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;

69
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<string, ItemUpdateInfo>();
publishedVersionMap = new Map<string, ItemPublishedVersionInfo>();
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<string, EntityType> = {
'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<AppState>,
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 {

10
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html

@ -100,15 +100,7 @@
<tb-icon>download</tb-icon>
{{ item.totalInstallCount | shortNumber }}
</span>
@if (installed) {
@if (hasUpdate()) {
<button class="tb-iot-hub-card-update-btn" (click)="onUpdateClick($event)">{{ 'iot-hub.update' | translate }}</button>
} @else {
<span class="tb-iot-hub-card-installed-label">{{ 'iot-hub.installed' | translate }}</span>
}
} @else {
<button class="tb-iot-hub-card-install-btn" (click)="onInstallClick($event)">{{ 'iot-hub.install' | translate }}</button>
}
<button class="tb-iot-hub-card-install-btn" (click)="onInstallClick($event)">{{ 'iot-hub.install' | translate }}</button>
</span>
</div>
</div>

27
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;
}
}
}

14
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<MpItemVersionView>();
@Output() creatorClick = new EventEmitter<string>();
@Output() installClick = new EventEmitter<MpItemVersionView>();
@Output() updateClick = new EventEmitter<MpItemVersionView>();
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);

11
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html

@ -175,20 +175,19 @@
<!-- Footer -->
<div class="dlg-footer">
<div class="dlg-footer-actions">
@if (installedDescriptor) {
@if (isSameVersion()) {
<button class="dlg-install-btn" (click)="openEntityDetails()">
{{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }}
</button>
} @else if (hasUpdate()) {
<button class="dlg-update-btn" (click)="updateItem()">
{{ 'iot-hub.update' | translate }}
</button>
} @else {
<button class="dlg-install-btn" (click)="install()">
{{ 'iot-hub.install-item-type' | translate:{ type: getTypeLabel() } }}
</button>
}
@if (hasUpdate()) {
<button class="dlg-update-btn" (click)="updateItem()">
{{ 'iot-hub.update' | translate }}
</button>
}
</div>
<div class="dlg-creator-info">
<span>{{ 'iot-hub.created-by' | translate:{ name: item.creatorDisplayName } }}</span>

54
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts

@ -19,7 +19,7 @@ import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dial
import { Router } from '@angular/router';
import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, nodeComponentTypeTranslations, NodeInfo } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType, itemTypeTranslations, getCategoriesForType, useCaseTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { IotHubInstalledItemDescriptor, IotHubInstalledItemInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
@ -30,8 +30,7 @@ import { TbIotHubUpdateDialogComponent, IotHubUpdateDialogData } from './iot-hub
export interface IotHubItemDetailDialogData {
item: MpItemVersionView;
iotHubApiService: IotHubApiService;
installedDescriptor?: IotHubInstalledItemDescriptor;
installedItemInfo?: IotHubInstalledItemInfo;
installedItem?: IotHubInstalledItem;
}
@Component({
@ -46,16 +45,7 @@ export class TbIotHubItemDetailDialogComponent {
item: MpItemVersionView;
typeTranslations = itemTypeTranslations;
readmeContent: string = '';
installedDescriptor?: IotHubInstalledItemDescriptor;
installedItemInfo?: IotHubInstalledItemInfo;
private static readonly ITEM_TYPE_TO_ENTITY_TYPE: Record<string, EntityType> = {
'WIDGET': EntityType.WIDGET_TYPE,
'DASHBOARD': EntityType.DASHBOARD,
'CALCULATED_FIELD': EntityType.CALCULATED_FIELD,
'RULE_CHAIN': EntityType.RULE_CHAIN,
'DEVICE': EntityType.DEVICE_PROFILE
};
installedItem?: IotHubInstalledItem;
private categoryMap: Map<string, string>;
private useCaseMap = useCaseTranslations;
@ -68,13 +58,7 @@ export class TbIotHubItemDetailDialogComponent {
private translate: TranslateService
) {
this.item = data.item;
this.installedDescriptor = data.installedDescriptor;
this.installedItemInfo = data.installedItemInfo;
if (!this.installedDescriptor && data.installedItemInfo) {
this.data.iotHubApiService.getInstalledItemByItemId(data.installedItemInfo.itemId, {ignoreLoading: true}).subscribe(
installedItem => this.installedDescriptor = installedItem?.descriptor
);
}
this.installedItem = data.installedItem;
this.categoryMap = getCategoriesForType(this.item.type);
this.loadReadme();
}
@ -232,9 +216,18 @@ export class TbIotHubItemDetailDialogComponent {
return this.item.dataDescriptor?.nodeCount || 0;
}
isInstalled(): boolean {
return this.installedItem != null;
}
isSameVersion(): boolean {
return this.installedItem != null
&& this.installedItem.itemVersionId === this.item.id;
}
hasUpdate(): boolean {
return this.installedItemInfo != null
&& this.installedItemInfo.itemVersionId !== this.item.id;
return this.installedItem != null
&& this.installedItem.itemVersionId !== this.item.id;
}
install(): void {
@ -256,7 +249,7 @@ export class TbIotHubItemDetailDialogComponent {
const dialogRef = this.dialog.open(TbIotHubUpdateDialogComponent, {
panelClass: ['tb-dialog'],
data: {
itemId: this.item.itemId,
installedItemId: this.installedItem.id.id,
itemName: this.item.name,
itemType: this.item.type,
version: this.item.version,
@ -281,18 +274,21 @@ export class TbIotHubItemDetailDialogComponent {
}
openEntityDetails(): void {
const descriptor = this.installedDescriptor;
const descriptor = this.installedItem?.descriptor;
if (!descriptor) {
return;
}
let entityId: string | null = null;
let entityType: EntityType | null = null;
switch (descriptor.type) {
case 'WIDGET': entityId = descriptor.widgetTypeId?.id; break;
case 'DASHBOARD': entityId = descriptor.dashboardId?.id; break;
case 'CALCULATED_FIELD': entityId = descriptor.calculatedFieldId?.id; break;
case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; break;
case 'WIDGET': entityId = descriptor.widgetTypeId?.id; entityType = EntityType.WIDGET_TYPE; break;
case 'DASHBOARD': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break;
case 'CALCULATED_FIELD':
entityId = descriptor.entityId?.id;
entityType = descriptor.entityId?.entityType as EntityType;
break;
case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; entityType = EntityType.RULE_CHAIN; break;
}
const entityType = TbIotHubItemDetailDialogComponent.ITEM_TYPE_TO_ENTITY_TYPE[this.item.type];
if (entityType && entityId) {
const url = getEntityDetailsPageURL(entityId, entityType);
if (url) {

7
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts

@ -21,7 +21,6 @@ import { Authority } from '@shared/models/authority.enum';
import { TbIotHubBrowseComponent } from './iot-hub-browse.component';
import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component';
import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component';
import { iotHubInstalledItemInfosResolver } from './iot-hub-installed-item-infos.resolver';
const routes: Routes = [
{
@ -40,9 +39,6 @@ const routes: Routes = [
data: {
auth: [Authority.TENANT_ADMIN],
title: 'iot-hub.browse'
},
resolve: {
installedItemInfos: iotHubInstalledItemInfosResolver
}
},
{
@ -55,9 +51,6 @@ const routes: Routes = [
label: 'iot-hub.installed-items',
icon: 'inventory_2'
}
},
resolve: {
installedItemInfos: iotHubInstalledItemInfosResolver
}
},
{

230
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-update-dialog.component.ts

@ -0,0 +1,230 @@
///
/// 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 { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { IotHubInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils';
export interface IotHubUpdateDialogData {
installedItemId: string;
itemName: string;
itemType: ItemType;
version: string;
versionId: string;
iotHubApiService: IotHubApiService;
}
export type UpdateState = 'confirm' | 'updating' | 'success' | 'error';
@Component({
selector: 'tb-iot-hub-update-dialog',
standalone: false,
template: `
@switch (state) {
@case ('confirm') {
<h2 mat-dialog-title>{{ 'iot-hub.update-item-title' | translate }}</h2>
<mat-dialog-content>
<p>{{ 'iot-hub.update-confirm' | translate:{ name: data.itemName, version: data.version } }}</p>
<p class="tb-iot-hub-install-meta">{{ 'iot-hub.install-type' | translate:{ type: getTypeLabel() } }}</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-raised-button color="primary" (click)="update()">{{ 'iot-hub.update' | translate }}</button>
</mat-dialog-actions>
}
@case ('updating') {
<h2 mat-dialog-title>{{ 'iot-hub.update-item-title' | translate }}</h2>
<mat-dialog-content>
<p>{{ 'iot-hub.update-confirm' | translate:{ name: data.itemName, version: data.version } }}</p>
<p class="tb-iot-hub-install-meta">{{ 'iot-hub.install-type' | translate:{ type: getTypeLabel() } }}</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button disabled>{{ 'action.cancel' | translate }}</button>
<button mat-raised-button color="primary" disabled>
<mat-spinner diameter="18" class="tb-iot-hub-inline-spinner"></mat-spinner>
{{ 'iot-hub.updating' | translate }}
</button>
</mat-dialog-actions>
}
@case ('success') {
<h2 mat-dialog-title>
<mat-icon class="tb-iot-hub-result-icon tb-iot-hub-success-icon">check_circle</mat-icon>
{{ 'iot-hub.update-success-title' | translate }}
</h2>
<mat-dialog-content>
<p>{{ 'iot-hub.update-success-message' | translate:{ name: data.itemName, version: data.version } }}</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">{{ 'action.close' | translate }}</button>
@if (entityDetailsUrl) {
<button mat-raised-button color="primary" (click)="openEntityDetails()">
{{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }}
</button>
}
</mat-dialog-actions>
}
@case ('error') {
<h2 mat-dialog-title>
<mat-icon class="tb-iot-hub-result-icon tb-iot-hub-error-icon">error</mat-icon>
{{ 'iot-hub.update-error-title' | translate }}
</h2>
<mat-dialog-content>
<p>{{ 'iot-hub.update-error-message' | translate:{ name: data.itemName } }}</p>
<div class="tb-iot-hub-error-details">{{ errorMessage }}</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">{{ 'action.close' | translate }}</button>
</mat-dialog-actions>
}
}
`,
styles: [`
.tb-iot-hub-install-meta {
margin: 4px 0;
color: rgba(0, 0, 0, 0.54);
font-size: 14px;
}
.tb-iot-hub-inline-spinner {
display: inline-block;
margin-right: 8px;
}
.tb-iot-hub-result-icon {
vertical-align: middle;
margin-right: 8px;
}
.tb-iot-hub-success-icon {
color: #2e7d32;
}
.tb-iot-hub-error-icon {
color: #c62828;
}
.tb-iot-hub-error-details {
max-height: 200px;
overflow-y: auto;
padding: 12px;
background: #f5f5f5;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
color: rgba(0, 0, 0, 0.7);
white-space: pre-wrap;
word-break: break-word;
margin-top: 8px;
}
`]
})
export class TbIotHubUpdateDialogComponent {
private static readonly ITEM_TYPE_TO_ENTITY_TYPE: Record<string, EntityType> = {
'WIDGET': EntityType.WIDGET_TYPE,
'DASHBOARD': EntityType.DASHBOARD,
'CALCULATED_FIELD': EntityType.CALCULATED_FIELD,
'RULE_CHAIN': EntityType.RULE_CHAIN,
'DEVICE': EntityType.DEVICE_PROFILE
};
typeTranslations = itemTypeTranslations;
state: UpdateState = 'confirm';
errorMessage = '';
entityDetailsUrl: string | null = null;
constructor(
@Inject(MAT_DIALOG_DATA) public data: IotHubUpdateDialogData,
private dialogRef: MatDialogRef<TbIotHubUpdateDialogComponent>,
private dialogService: DialogService,
private router: Router,
private translate: TranslateService
) {}
getTypeLabel(): string {
const key = this.typeTranslations.get(this.data.itemType);
return key ? this.translate.instant(key) : '';
}
update(force = false): void {
this.state = 'updating';
this.data.iotHubApiService.updateItemVersion(this.data.installedItemId, this.data.versionId, { ignoreLoading: true }, force).subscribe({
next: (result) => {
if (result.success) {
this.state = 'success';
this.entityDetailsUrl = this.resolveEntityDetailsUrl(result.descriptor);
} else if (result.entityModified) {
this.state = 'confirm';
this.dialogService.confirm(
this.translate.instant('iot-hub.entity-modified-title'),
this.translate.instant('iot-hub.entity-modified-text', { type: this.getTypeLabel() }),
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe(confirmed => {
if (confirmed) {
this.update(true);
}
});
} else {
this.state = 'error';
this.errorMessage = result.errorMessage || this.translate.instant('iot-hub.update-error', { name: this.data.itemName });
}
},
error: (err) => {
this.state = 'error';
this.errorMessage = err?.error?.message || err?.message || this.translate.instant('iot-hub.update-error', { name: this.data.itemName });
}
});
}
openEntityDetails(): void {
if (this.entityDetailsUrl) {
this.dialogRef.close('updated');
this.router.navigateByUrl(this.entityDetailsUrl);
}
}
close(): void {
this.dialogRef.close(this.state === 'success' ? 'updated' : false);
}
cancel(): void {
this.dialogRef.close(false);
}
private resolveEntityDetailsUrl(descriptor: IotHubInstalledItemDescriptor): string | null {
if (!descriptor) {
return null;
}
const entityType = TbIotHubUpdateDialogComponent.ITEM_TYPE_TO_ENTITY_TYPE[this.data.itemType];
if (!entityType) {
return null;
}
let entityId: string | null = null;
switch (descriptor.type) {
case 'WIDGET': entityId = descriptor.widgetTypeId?.id; break;
case 'DASHBOARD': entityId = descriptor.dashboardId?.id; break;
case 'CALCULATED_FIELD': entityId = descriptor.calculatedFieldId?.id; break;
case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; break;
}
if (!entityId) {
return null;
}
return getEntityDetailsPageURL(entityId, entityType) || null;
}
}

12
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -298,6 +298,18 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
this.entityRequiredText = 'ai-models.model-required';
this.notFoundEntities = 'ai-models.no-model-text';
break;
case EntityType.DEVICE_PROFILE:
this.entityText = 'device-profile.device-profile';
this.noEntitiesMatchingText = 'device-profile.no-device-profiles-matching';
this.entityRequiredText = 'device-profile.device-profile-required';
this.notFoundEntities = 'device-profile.no-device-profiles-text';
break;
case EntityType.ASSET_PROFILE:
this.entityText = 'asset-profile.asset-profile';
this.noEntitiesMatchingText = 'asset-profile.no-asset-profiles-matching';
this.entityRequiredText = 'asset-profile.asset-profile-required';
this.notFoundEntities = 'asset-profile.no-asset-profiles-text';
break;
case AliasEntityType.CURRENT_CUSTOMER:
this.entityText = 'customer.default-customer';
this.noEntitiesMatchingText = 'customer.no-customers-matching';

1
ui-ngx/src/app/shared/components/entity/entity-select.component.html

@ -25,6 +25,7 @@
[useAliasEntityTypes]="useAliasEntityTypes"
[allowedEntityTypes]="allowedEntityTypes"
[additionEntityTypes]="additionEntityTypes"
[filterAllowedEntityTypes]="filterAllowedEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
<tb-entity-autocomplete

27
ui-ngx/src/app/shared/components/entity/entity-select.component.ts

@ -63,6 +63,12 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
filterAllowedEntityTypes = true;
@Input()
defaultEntityType: AliasEntityType | EntityType;
displayEntityTypeSelect: boolean;
AliasEntityType = AliasEntityType;
@ -71,8 +77,6 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
AliasEntityType.CURRENT_TENANT, AliasEntityType.CURRENT_USER, AliasEntityType.CURRENT_USER_OWNER
]);
private readonly defaultEntityType: EntityType | AliasEntityType = null;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
@ -83,15 +87,17 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
const entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes,
this.useAliasEntityTypes);
let defaultEntityType: EntityType | AliasEntityType = null;
if (entityTypes.length === 1) {
this.displayEntityTypeSelect = false;
this.defaultEntityType = entityTypes[0];
defaultEntityType = entityTypes[0];
} else {
this.displayEntityTypeSelect = true;
}
this.entitySelectFormGroup = this.fb.group({
entityType: [this.defaultEntityType],
entityType: [defaultEntityType],
entityId: [null]
});
}
@ -123,6 +129,19 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
if (additionNullUIIDEntityTypes.length > 0) {
additionNullUIIDEntityTypes.forEach((entityType) => this.entityTypeNullUUID.add(entityType));
}
if (this.filterAllowedEntityTypes === false) {
if (this.allowedEntityTypes?.length === 1) {
this.displayEntityTypeSelect = false;
this.entitySelectFormGroup.get('entityType').setValue(this.allowedEntityTypes[0]);
} else {
this.displayEntityTypeSelect = true;
}
}
if (this.defaultEntityType) {
this.entitySelectFormGroup.get('entityType').setValue(this.defaultEntityType);
}
}
ngAfterViewInit(): void {

14
ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts

@ -29,6 +29,7 @@ export interface DashboardInstalledItemDescriptor {
export interface CalculatedFieldInstalledItemDescriptor {
type: 'CALCULATED_FIELD';
calculatedFieldId: { id: string };
entityId: { entityType: string; id: string };
}
export interface RuleChainInstalledItemDescriptor {
@ -55,20 +56,15 @@ export interface InstallItemVersionResult {
export interface UpdateItemVersionResult {
success: boolean;
entityModified: boolean;
errorMessage: string;
descriptor: IotHubInstalledItemDescriptor;
}
export interface IotHubInstalledItemInfo {
export interface ItemPublishedVersionInfo {
itemId: string;
itemVersionId: string;
}
export interface ItemUpdateInfo {
itemId: string;
hasUpdate: boolean;
latestVersion: string;
latestItemVersionId: string;
publishedVersionId: string;
publishedVersion: string;
}
export interface IotHubInstalledItem extends BaseData<{id: string}> {

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3734,6 +3734,7 @@
"created-by": "Created by {{name}}",
"install-item-title": "Install Item",
"install-confirm": "Install {{name}} v{{version}}?",
"select-entity-for-cf": "Select entity for calculated field '{{name}}'",
"install-type": "Type: {{type}}",
"install-creator": "Creator: {{creator}}",
"install": "Install",
@ -3754,6 +3755,8 @@
"updating": "Updating...",
"update-success-title": "Update Successful",
"update-success-message": "'{{name}}' has been updated to version {{version}}.",
"entity-modified-title": "Entity Modified",
"entity-modified-text": "The {{type}} has been modified locally. Do you still want to update it?",
"update-error-title": "Update Failed",
"update-error-message": "Failed to update '{{name}}'.",
"update-error": "Failed to update {{name}}",

Loading…
Cancel
Save