Browse Source

Add IoT Hub installed items management

- Full entity stack: IotHubInstalledItem with ID, descriptors, JPA entity, DAO (extends JpaAbstractDao), service
- Controller endpoints: install, list (paginated), list infos, get by itemId, delete
- Install flow saves descriptor with entity IDs, delete cascades to installed entities
- Frontend installed items page with search, pagination, type chips, action buttons
- Browse page shows "Installed" label on cards, detail dialog shows "Open details" for installed items
- Installed item infos fetched on browse page init to track installation state
pull/15347/head
Igor Kulikov 7 months ago
parent
commit
285c862553
  1. 19
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 49
      application/src/main/java/org/thingsboard/server/controller/IotHubController.java
  3. 172
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  4. 40
      application/src/main/java/org/thingsboard/server/service/iot_hub/InstallItemVersionResult.java
  5. 7
      application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java
  6. 34
      common/data/src/main/java/org/thingsboard/server/common/data/id/IotHubInstalledItemId.java
  7. 26
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java
  8. 26
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DashboardInstalledItemDescriptor.java
  9. 23
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java
  10. 47
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItem.java
  11. 38
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java
  12. 32
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemInfo.java
  13. 26
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java
  14. 26
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/WidgetInstalledItemDescriptor.java
  15. 41
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java
  16. 42
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java
  17. 69
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java
  18. 12
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  19. 110
      dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java
  20. 60
      dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java
  21. 100
      dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java
  22. 3
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  23. 13
      dao/src/main/resources/sql/schema-entities.sql
  24. 37
      ui-ngx/src/app/core/http/iot-hub-api.service.ts
  25. 11
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html
  26. 3
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss
  27. 37
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts
  28. 25
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts
  29. 116
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html
  30. 202
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss
  31. 210
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts
  32. 6
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html
  33. 10
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss
  34. 1
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts
  35. 12
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html
  36. 49
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts
  37. 13
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts
  38. 4
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts
  39. 63
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts
  40. 18
      ui-ngx/src/assets/locale/locale.constant-en_US.json

19
application/src/main/data/upgrade/basic/schema_update.sql

@ -43,4 +43,21 @@ DROP INDEX IF EXISTS idx_widgets_bundle_external_id;
-- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END -- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END
ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255); ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255);
-- IOT HUB INSTALLED ITEM START
CREATE TABLE IF NOT EXISTS iot_hub_installed_item (
id UUID NOT NULL PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
item_id UUID NOT NULL,
item_version_id UUID NOT NULL,
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)
);
-- IOT HUB INSTALLED ITEM END

49
application/src/main/java/org/thingsboard/server/controller/IotHubController.java

@ -15,18 +15,29 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; 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.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException; 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.dao.iot_hub.IotHubInstalledItemService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.iot_hub.InstallItemVersionResult;
import org.thingsboard.server.service.iot_hub.IotHubService; import org.thingsboard.server.service.iot_hub.IotHubService;
@Hidden @Hidden
@ -38,11 +49,45 @@ import org.thingsboard.server.service.iot_hub.IotHubService;
public class IotHubController extends BaseController { public class IotHubController extends BaseController {
private final IotHubService iotHubService; private final IotHubService iotHubService;
private final IotHubInstalledItemService iotHubInstalledItemService;
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/versions/{versionId}/install") @PostMapping("/versions/{versionId}/install")
@ResponseBody @ResponseBody
public JsonNode installItemVersion(@PathVariable String versionId) throws Exception { public InstallItemVersionResult installItemVersion(@PathVariable String versionId) throws ThingsboardException {
return iotHubService.installItemVersion(getCurrentUser(), versionId); return iotHubService.installItemVersion(getCurrentUser(), versionId);
} }
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/installedItems")
@ResponseBody
public PageData<IotHubInstalledItem> getInstalledItems(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return iotHubInstalledItemService.findByTenantId(getTenantId(), pageLink);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/installedItems/byItemId/{itemId}")
@ResponseBody
public IotHubInstalledItem getInstalledItemByItemId(@PathVariable UUID itemId) throws ThingsboardException {
return iotHubInstalledItemService.findByTenantIdAndItemId(getTenantId(), itemId).orElse(null);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/installedItems/info")
@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);
}
} }

172
application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java

@ -24,8 +24,23 @@ import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.TenantId; 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;
import org.thingsboard.server.common.data.iot_hub.DeviceInstalledItemDescriptor;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem;
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.RuleChainData;
import org.thingsboard.server.common.data.rule.RuleChainImportResult;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService;
@ -33,7 +48,9 @@ import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileServ
import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService;
import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.dao.rule.RuleChainService;
import java.util.List;
import java.util.UUID;
@Service @Service
@TbCoreComponent @TbCoreComponent
@ -48,81 +65,148 @@ public class DefaultIotHubService implements IotHubService {
private final RuleChainService ruleChainService; private final RuleChainService ruleChainService;
private final TbRuleChainService tbRuleChainService; private final TbRuleChainService tbRuleChainService;
private final TbDeviceProfileService tbDeviceProfileService; private final TbDeviceProfileService tbDeviceProfileService;
private final IotHubInstalledItemService iotHubInstalledItemService;
private final WidgetTypeService widgetTypeService;
private final DashboardService dashboardService;
private final CalculatedFieldService calculatedFieldService;
private final DeviceProfileService deviceProfileService;
@Override @Override
public JsonNode installItemVersion(SecurityUser user, String versionId) throws Exception { public InstallItemVersionResult installItemVersion(SecurityUser user, String versionId) {
TenantId tenantId = user.getTenantId(); TenantId tenantId = user.getTenantId();
log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId); log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId);
JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); try {
String itemType = versionInfo.get("type").asText(); JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId);
String itemName = versionInfo.get("name").asText(); String itemType = versionInfo.get("type").asText();
log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType); String itemName = versionInfo.get("name").asText();
UUID itemId = UUID.fromString(versionInfo.get("itemId").asText());
byte[] fileData = iotHubRestClient.getVersionFileData(versionId); String version = versionInfo.get("version").asText();
log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0); log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType);
switch (itemType) { if (iotHubInstalledItemService.findByTenantIdAndItemId(tenantId, itemId).isPresent()) {
case "WIDGET": return InstallItemVersionResult.error("Item '" + itemName + "' is already installed");
installWidget(user, tenantId, fileData); }
break;
case "DASHBOARD":
installDashboard(user, tenantId, fileData);
break;
case "CALCULATED_FIELD":
installCalculatedField(user, tenantId, fileData);
break;
case "RULE_CHAIN":
installRuleChain(tenantId, fileData);
break;
case "DEVICE":
installDeviceProfile(user, tenantId, fileData);
break;
default:
throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType);
}
iotHubRestClient.reportVersionInstalled(versionId); byte[] fileData = iotHubRestClient.getVersionFileData(versionId);
log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType); log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0);
return versionInfo; IotHubInstalledItemDescriptor descriptor = switch (itemType) {
case "WIDGET" -> installWidget(user, tenantId, fileData);
case "DASHBOARD" -> installDashboard(user, tenantId, fileData);
case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData);
case "RULE_CHAIN" -> installRuleChain(tenantId, fileData);
case "DEVICE" -> installDeviceProfile(user, tenantId, fileData);
default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType);
};
IotHubInstalledItem installedItem = new IotHubInstalledItem();
installedItem.setTenantId(tenantId);
installedItem.setItemId(itemId);
installedItem.setItemVersionId(UUID.fromString(versionId));
installedItem.setItemName(itemName);
installedItem.setItemType(itemType);
installedItem.setVersion(version);
installedItem.setDescriptor(descriptor);
iotHubInstalledItemService.save(tenantId, installedItem);
iotHubRestClient.reportVersionInstalled(versionId);
log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType);
return InstallItemVersionResult.success(descriptor);
} catch (Exception e) {
log.error("[{}] Failed to install IoT Hub item version: {}", tenantId, versionId, e);
return InstallItemVersionResult.error(e.getMessage());
}
} }
private void installWidget(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { private WidgetInstalledItemDescriptor installWidget(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception {
WidgetTypeDetails widgetTypeDetails = JacksonUtil.fromBytes(fileData, WidgetTypeDetails.class); WidgetTypeDetails widgetTypeDetails = JacksonUtil.fromBytes(fileData, WidgetTypeDetails.class);
widgetTypeDetails.setId(null); widgetTypeDetails.setId(null);
widgetTypeDetails.setTenantId(tenantId); widgetTypeDetails.setTenantId(tenantId);
tbWidgetTypeService.save(widgetTypeDetails, true, user); WidgetTypeDetails saved = tbWidgetTypeService.save(widgetTypeDetails, true, user);
log.debug("[{}] Widget installed: {}", tenantId, widgetTypeDetails.getName()); log.debug("[{}] Widget installed: {}", tenantId, saved.getName());
WidgetInstalledItemDescriptor descriptor = new WidgetInstalledItemDescriptor();
descriptor.setWidgetTypeId(saved.getId());
return descriptor;
} }
private void installDashboard(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { private DashboardInstalledItemDescriptor installDashboard(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception {
Dashboard dashboard = JacksonUtil.fromBytes(fileData, Dashboard.class); Dashboard dashboard = JacksonUtil.fromBytes(fileData, Dashboard.class);
dashboard.setId(null); dashboard.setId(null);
dashboard.setTenantId(tenantId); dashboard.setTenantId(tenantId);
tbDashboardService.save(dashboard, user); Dashboard saved = tbDashboardService.save(dashboard, user);
log.debug("[{}] Dashboard installed: {}", tenantId, dashboard.getTitle()); log.debug("[{}] Dashboard installed: {}", tenantId, saved.getTitle());
DashboardInstalledItemDescriptor descriptor = new DashboardInstalledItemDescriptor();
descriptor.setDashboardId(saved.getId());
return descriptor;
} }
private void installCalculatedField(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { private CalculatedFieldInstalledItemDescriptor installCalculatedField(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception {
CalculatedField calculatedField = JacksonUtil.fromBytes(fileData, CalculatedField.class); CalculatedField calculatedField = JacksonUtil.fromBytes(fileData, CalculatedField.class);
calculatedField.setId(null); calculatedField.setId(null);
calculatedField.setTenantId(tenantId); calculatedField.setTenantId(tenantId);
tbCalculatedFieldService.save(calculatedField, user); CalculatedField saved = tbCalculatedFieldService.save(calculatedField, user);
log.debug("[{}] Calculated field installed: {}", tenantId, calculatedField.getName()); log.debug("[{}] Calculated field installed: {}", tenantId, saved.getName());
CalculatedFieldInstalledItemDescriptor descriptor = new CalculatedFieldInstalledItemDescriptor();
descriptor.setCalculatedFieldId(saved.getId());
return descriptor;
} }
private void installRuleChain(TenantId tenantId, byte[] fileData) { private RuleChainInstalledItemDescriptor installRuleChain(TenantId tenantId, byte[] fileData) {
RuleChainData ruleChainData = JacksonUtil.fromBytes(fileData, RuleChainData.class); RuleChainData ruleChainData = JacksonUtil.fromBytes(fileData, RuleChainData.class);
ruleChainService.importTenantRuleChains(tenantId, ruleChainData, false, tbRuleChainService::updateRuleNodeConfiguration); List<RuleChainImportResult> results = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, false, tbRuleChainService::updateRuleNodeConfiguration);
log.debug("[{}] Rule chain(s) installed", tenantId); log.debug("[{}] Rule chain(s) installed", tenantId);
RuleChainInstalledItemDescriptor descriptor = new RuleChainInstalledItemDescriptor();
if (!results.isEmpty()) {
descriptor.setRuleChainId(results.get(0).getRuleChainId());
}
return descriptor;
} }
private void installDeviceProfile(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { private DeviceInstalledItemDescriptor installDeviceProfile(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception {
DeviceProfile deviceProfile = JacksonUtil.fromBytes(fileData, DeviceProfile.class); DeviceProfile deviceProfile = JacksonUtil.fromBytes(fileData, DeviceProfile.class);
deviceProfile.setId(null); deviceProfile.setId(null);
deviceProfile.setTenantId(tenantId); deviceProfile.setTenantId(tenantId);
tbDeviceProfileService.save(deviceProfile, user); tbDeviceProfileService.save(deviceProfile, user);
log.debug("[{}] Device profile installed: {}", tenantId, deviceProfile.getName()); log.debug("[{}] Device profile installed: {}", tenantId, deviceProfile.getName());
return new DeviceInstalledItemDescriptor();
}
@Override
public void deleteInstalledItem(SecurityUser user, UUID itemId) {
TenantId tenantId = user.getTenantId();
IotHubInstalledItem installedItem = iotHubInstalledItemService.findByTenantIdAndItemId(tenantId, itemId)
.orElseThrow(() -> new IllegalArgumentException("Installed item not found"));
IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor();
if (descriptor instanceof WidgetInstalledItemDescriptor wd) {
WidgetTypeDetails widgetType = widgetTypeService.findWidgetTypeDetailsById(tenantId, wd.getWidgetTypeId());
if (widgetType != null) {
tbWidgetTypeService.delete(widgetType, user);
}
} else if (descriptor instanceof DashboardInstalledItemDescriptor dd) {
Dashboard dashboard = dashboardService.findDashboardById(tenantId, dd.getDashboardId());
if (dashboard != null) {
tbDashboardService.delete(dashboard, user);
}
} else if (descriptor instanceof CalculatedFieldInstalledItemDescriptor cd) {
CalculatedField calculatedField = calculatedFieldService.findById(tenantId, cd.getCalculatedFieldId());
if (calculatedField != null) {
tbCalculatedFieldService.delete(calculatedField, user);
}
} else if (descriptor instanceof RuleChainInstalledItemDescriptor rd) {
if (rd.getRuleChainId() != null) {
RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, rd.getRuleChainId());
if (ruleChain != null) {
tbRuleChainService.delete(ruleChain, user);
}
}
} else if (descriptor instanceof DeviceInstalledItemDescriptor) {
// no entity to delete for now
}
iotHubInstalledItemService.deleteByTenantIdAndItemId(tenantId, itemId);
log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName());
} }
} }

40
application/src/main/java/org/thingsboard/server/service/iot_hub/InstallItemVersionResult.java

@ -0,0 +1,40 @@
/**
* 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.service.iot_hub;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class InstallItemVersionResult {
private boolean success;
private String errorMessage;
private IotHubInstalledItemDescriptor descriptor;
public static InstallItemVersionResult success(IotHubInstalledItemDescriptor descriptor) {
return new InstallItemVersionResult(true, null, descriptor);
}
public static InstallItemVersionResult error(String errorMessage) {
return new InstallItemVersionResult(false, errorMessage, null);
}
}

7
application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java

@ -15,10 +15,13 @@
*/ */
package org.thingsboard.server.service.iot_hub; package org.thingsboard.server.service.iot_hub;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.UUID;
public interface IotHubService { public interface IotHubService {
JsonNode installItemVersion(SecurityUser user, String versionId) throws Exception; InstallItemVersionResult installItemVersion(SecurityUser user, String versionId);
void deleteInstalledItem(SecurityUser user, UUID itemId);
} }

34
common/data/src/main/java/org/thingsboard/server/common/data/id/IotHubInstalledItemId.java

@ -0,0 +1,34 @@
/**
* 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.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.UUID;
public class IotHubInstalledItemId extends UUIDBased {
@JsonCreator
public IotHubInstalledItemId(@JsonProperty("id") UUID id) {
super(id);
}
public static IotHubInstalledItemId fromString(String id) {
return new IotHubInstalledItemId(UUID.fromString(id));
}
}

26
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java

@ -0,0 +1,26 @@
/**
* 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.Data;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@Data
public class CalculatedFieldInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private CalculatedFieldId calculatedFieldId;
}

26
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DashboardInstalledItemDescriptor.java

@ -0,0 +1,26 @@
/**
* 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.Data;
import org.thingsboard.server.common.data.id.DashboardId;
@Data
public class DashboardInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private DashboardId dashboardId;
}

23
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java

@ -0,0 +1,23 @@
/**
* 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.Data;
@Data
public class DeviceInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
}

47
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItem.java

@ -0,0 +1,47 @@
/**
* 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.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)
public class IotHubInstalledItem extends BaseData<IotHubInstalledItemId> implements HasTenantId {
private TenantId tenantId;
private UUID itemId;
private UUID itemVersionId;
private String itemName;
private String itemType;
private String version;
private IotHubInstalledItemDescriptor descriptor;
public IotHubInstalledItem() {
super();
}
public IotHubInstalledItem(IotHubInstalledItemId id) {
super(id);
}
}

38
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java

@ -0,0 +1,38 @@
/**
* 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.io.Serializable;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type")
@JsonSubTypes({
@Type(name = "WIDGET", value = WidgetInstalledItemDescriptor.class),
@Type(name = "DASHBOARD", value = DashboardInstalledItemDescriptor.class),
@Type(name = "CALCULATED_FIELD", value = CalculatedFieldInstalledItemDescriptor.class),
@Type(name = "RULE_CHAIN", value = RuleChainInstalledItemDescriptor.class),
@Type(name = "DEVICE", value = DeviceInstalledItemDescriptor.class)
})
public interface IotHubInstalledItemDescriptor extends Serializable {
}

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

@ -0,0 +1,32 @@
/**
* 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;
}

26
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java

@ -0,0 +1,26 @@
/**
* 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.Data;
import org.thingsboard.server.common.data.id.RuleChainId;
@Data
public class RuleChainInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private RuleChainId ruleChainId;
}

26
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/WidgetInstalledItemDescriptor.java

@ -0,0 +1,26 @@
/**
* 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.Data;
import org.thingsboard.server.common.data.id.WidgetTypeId;
@Data
public class WidgetInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private WidgetTypeId widgetTypeId;
}

41
dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java

@ -0,0 +1,41 @@
/**
* 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.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);
void deleteByTenantId(TenantId tenantId);
}

42
dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java

@ -0,0 +1,42 @@
/**
* 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.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 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);
PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, PageLink pageLink);
List<IotHubInstalledItemInfo> findInstalledItemInfosByTenantId(TenantId tenantId);
boolean deleteByTenantIdAndItemId(TenantId tenantId, UUID itemId);
void deleteByTenantId(TenantId tenantId);
}

69
dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java

@ -0,0 +1,69 @@
/**
* 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.dao.iot_hub;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
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
@RequiredArgsConstructor
@Slf4j
class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService {
private final IotHubInstalledItemDao iotHubInstalledItemDao;
@Override
public IotHubInstalledItem save(TenantId tenantId, IotHubInstalledItem item) {
log.debug("[{}] Saving IoT Hub installed item: {}", tenantId, item);
return iotHubInstalledItemDao.save(tenantId, item);
}
@Override
public Optional<IotHubInstalledItem> findByTenantIdAndItemId(TenantId tenantId, UUID itemId) {
return iotHubInstalledItemDao.findByTenantIdAndItemId(tenantId, itemId);
}
@Override
public PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, PageLink pageLink) {
return iotHubInstalledItemDao.findByTenantId(tenantId, pageLink);
}
@Override
public List<IotHubInstalledItemInfo> findInstalledItemInfosByTenantId(TenantId tenantId) {
return iotHubInstalledItemDao.findInstalledItemInfosByTenantId(tenantId);
}
@Override
public boolean deleteByTenantIdAndItemId(TenantId tenantId, UUID itemId) {
return iotHubInstalledItemDao.deleteByTenantIdAndItemId(tenantId, itemId);
}
@Override
public void deleteByTenantId(TenantId tenantId) {
iotHubInstalledItemDao.deleteByTenantId(tenantId);
}
}

12
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -760,6 +760,18 @@ public class ModelConstants {
public static final String AI_MODEL_NAME_COLUMN_NAME = NAME_PROPERTY; public static final String AI_MODEL_NAME_COLUMN_NAME = NAME_PROPERTY;
public static final String AI_MODEL_CONFIGURATION_COLUMN_NAME = "configuration"; public static final String AI_MODEL_CONFIGURATION_COLUMN_NAME = "configuration";
/**
* IoT Hub installed item constants.
*/
public static final String IOT_HUB_INSTALLED_ITEM_TABLE_NAME = "iot_hub_installed_item";
public static final String IOT_HUB_INSTALLED_ITEM_TENANT_ID_COLUMN = TENANT_ID_COLUMN;
public static final String IOT_HUB_INSTALLED_ITEM_ITEM_ID_COLUMN = "item_id";
public static final String IOT_HUB_INSTALLED_ITEM_ITEM_VERSION_ID_COLUMN = "item_version_id";
public static final String IOT_HUB_INSTALLED_ITEM_ITEM_NAME_COLUMN = "item_name";
public static final String IOT_HUB_INSTALLED_ITEM_ITEM_TYPE_COLUMN = "item_type";
public static final String IOT_HUB_INSTALLED_ITEM_VERSION_COLUMN = "version";
public static final String IOT_HUB_INSTALLED_ITEM_DESCRIPTOR_COLUMN = "descriptor";
protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};
protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)}; protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)};

110
dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java

@ -0,0 +1,110 @@
/**
* 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.dao.model.sql;
import io.hypersistence.utils.hibernate.type.json.JsonBinaryType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.Type;
import org.hibernate.proxy.HibernateProxy;
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.IotHubInstalledItemDescriptor;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import java.util.Objects;
import java.util.UUID;
@Getter
@Setter
@ToString
@Entity
@Table(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_TABLE_NAME)
public class IotHubInstalledItemEntity extends BaseSqlEntity<IotHubInstalledItem> {
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_TENANT_ID_COLUMN, nullable = false, columnDefinition = "UUID")
private UUID tenantId;
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_ID_COLUMN, nullable = false, columnDefinition = "UUID")
private UUID itemId;
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_VERSION_ID_COLUMN, nullable = false, columnDefinition = "UUID")
private UUID itemVersionId;
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_NAME_COLUMN, nullable = false)
private String itemName;
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_TYPE_COLUMN, nullable = false)
private String itemType;
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_VERSION_COLUMN, nullable = false)
private String version;
@Type(JsonBinaryType.class)
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_DESCRIPTOR_COLUMN, nullable = false, columnDefinition = "JSONB")
private IotHubInstalledItemDescriptor descriptor;
public IotHubInstalledItemEntity() {
}
public IotHubInstalledItemEntity(IotHubInstalledItem item) {
super(item);
tenantId = getTenantUuid(item.getTenantId());
itemId = item.getItemId();
itemVersionId = item.getItemVersionId();
itemName = item.getItemName();
itemType = item.getItemType();
version = item.getVersion();
descriptor = item.getDescriptor();
}
@Override
public IotHubInstalledItem toData() {
var item = new IotHubInstalledItem(new IotHubInstalledItemId(id));
item.setCreatedTime(createdTime);
item.setTenantId(TenantId.fromUUID(tenantId));
item.setItemId(itemId);
item.setItemVersionId(itemVersionId);
item.setItemName(itemName);
item.setItemType(itemType);
item.setVersion(version);
item.setDescriptor(descriptor);
return item;
}
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass) return false;
IotHubInstalledItemEntity that = (IotHubInstalledItemEntity) o;
return getId() != null && Objects.equals(getId(), that.getId());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
}
}

60
dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java

@ -0,0 +1,60 @@
/**
* 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.dao.sql.iot_hub;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
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 item FROM IotHubInstalledItemEntity item
WHERE item.tenantId = :tenantId
AND (:textSearch IS NULL
OR ilike(item.itemName, CONCAT('%', :textSearch, '%')) = true
OR ilike(item.itemType, CONCAT('%', :textSearch, '%')) = true
OR ilike(item.version, CONCAT('%', :textSearch, '%')) = true)
""")
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)
void deleteByTenantId(@Param("tenantId") UUID tenantId);
}

100
dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java

@ -0,0 +1,100 @@
/**
* 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.dao.sql.iot_hub;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
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;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemDao;
import org.thingsboard.server.dao.model.sql.IotHubInstalledItemEntity;
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
@Component
@RequiredArgsConstructor
class JpaIotHubInstalledItemDao extends JpaAbstractDao<IotHubInstalledItemEntity, IotHubInstalledItem> implements IotHubInstalledItemDao {
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(
tenantId.getId(),
StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null),
toPageRequest(pageLink))
);
}
@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());
}
@Override
public void deleteByTenantId(TenantId tenantId) {
repository.deleteByTenantId(tenantId.getId());
}
private static PageRequest toPageRequest(PageLink pageLink) {
Sort sort;
SortOrder sortOrder = pageLink.getSortOrder();
if (sortOrder == null) {
sort = Sort.by(Sort.Direction.DESC, "createdTime");
} else {
sort = Sort.by(
Sort.Direction.fromString(sortOrder.getDirection().name()),
sortOrder.getProperty()
).and(Sort.by(Sort.Direction.ASC, "id"));
}
return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), sort);
}
@Override
protected Class<IotHubInstalledItemEntity> getEntityClass() {
return IotHubInstalledItemEntity.class;
}
@Override
protected JpaRepository<IotHubInstalledItemEntity, UUID> getRepository() {
return repository;
}
}

3
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -83,6 +83,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
@Autowired @Autowired
private TrendzSettingsService trendzSettingsService; private TrendzSettingsService trendzSettingsService;
@Autowired @Autowired
private org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService iotHubInstalledItemService;
@Autowired
private TenantDataValidator tenantValidator; private TenantDataValidator tenantValidator;
@Autowired @Autowired
protected TbTransactionalCache<TenantId, Boolean> existsTenantCache; protected TbTransactionalCache<TenantId, Boolean> existsTenantCache;
@ -168,6 +170,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
notificationSettingsService.deleteNotificationSettings(tenantId); notificationSettingsService.deleteNotificationSettings(tenantId);
trendzSettingsService.deleteTrendzSettings(tenantId); trendzSettingsService.deleteTrendzSettings(tenantId);
qrCodeSettingService.deleteByTenantId(tenantId); qrCodeSettingService.deleteByTenantId(tenantId);
iotHubInstalledItemService.deleteByTenantId(tenantId);
tenantDao.removeById(tenantId, tenantId.getId()); tenantDao.removeById(tenantId, tenantId.getId());
publishEvictEvent(new TenantEvictEvent(tenantId, true)); publishEvictEvent(new TenantEvictEvent(tenantId, true));

13
dao/src/main/resources/sql/schema-entities.sql

@ -976,3 +976,16 @@ CREATE TABLE IF NOT EXISTS ai_model (
CONSTRAINT ai_model_name_unq_key UNIQUE (tenant_id, name), CONSTRAINT ai_model_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT ai_model_external_id_unq_key UNIQUE (tenant_id, external_id) CONSTRAINT ai_model_external_id_unq_key UNIQUE (tenant_id, external_id)
); );
CREATE TABLE IF NOT EXISTS iot_hub_installed_item (
id UUID NOT NULL PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
item_id UUID NOT NULL,
item_version_id UUID NOT NULL,
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)
);

37
ui-ngx/src/app/core/http/iot-hub-api.service.ts

@ -19,8 +19,10 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data'; 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 { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { CreatorView } from '@shared/models/iot-hub/iot-hub-creator.models'; import { CreatorView } from '@shared/models/iot-hub/iot-hub-creator.models';
import { IotHubInstalledItem, IotHubInstalledItemInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params'; import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params';
import { InterceptorConfig } from '@core/interceptors/interceptor-config'; import { InterceptorConfig } from '@core/interceptors/interceptor-config';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -81,6 +83,13 @@ export class IotHubApiService {
); );
} }
public getVersionInfo(versionId: string, config?: IotHubRequestConfig): Observable<MpItemVersionView> {
return this.http.get<MpItemVersionView>(
`${this.baseUrl}/api/versions/${versionId}`,
{ params: this.buildParams(config) }
);
}
public getVersionReadme(versionId: string, config?: IotHubRequestConfig): Observable<string> { public getVersionReadme(versionId: string, config?: IotHubRequestConfig): Observable<string> {
return this.http.get(`${this.baseUrl}/api/versions/${versionId}/readme`, { return this.http.get(`${this.baseUrl}/api/versions/${versionId}/readme`, {
params: this.buildParams(config), params: this.buildParams(config),
@ -111,6 +120,34 @@ export class IotHubApiService {
); );
} }
public getInstalledItemByItemId(itemId: string, config?: IotHubRequestConfig): Observable<IotHubInstalledItem> {
return this.http.get<IotHubInstalledItem>(
`/api/iot-hub/installedItems/byItemId/${itemId}`,
{ params: this.buildParams(config) }
);
}
public getInstalledItemInfos(config?: IotHubRequestConfig): Observable<IotHubInstalledItemInfo[]> {
return this.http.get<IotHubInstalledItemInfo[]>(
`/api/iot-hub/installedItems/info`,
{ params: this.buildParams(config) }
);
}
public getInstalledItems(pageLink: PageLink, config?: IotHubRequestConfig): Observable<PageData<IotHubInstalledItem>> {
return this.http.get<PageData<IotHubInstalledItem>>(
`/api/iot-hub/installedItems${pageLink.toQuery()}`,
{ params: this.buildParams(config) }
);
}
public deleteInstalledItem(itemId: string, config?: IotHubRequestConfig): Observable<void> {
return this.http.delete<void>(
`/api/iot-hub/installedItems/${itemId}`,
{ params: this.buildParams(config) }
);
}
public getCreatorProfile(creatorId: string, config?: IotHubRequestConfig): Observable<CreatorView> { public getCreatorProfile(creatorId: string, config?: IotHubRequestConfig): Observable<CreatorView> {
return this.http.get<CreatorView>( return this.http.get<CreatorView>(
`${this.baseUrl}/api/creators/${creatorId}/profile`, `${this.baseUrl}/api/creators/${creatorId}/profile`,

11
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html

@ -19,8 +19,14 @@
<!-- Page header --> <!-- Page header -->
@if (!embedded) { @if (!embedded) {
<div class="tb-iot-hub-browse-header"> <div class="tb-iot-hub-browse-header">
<h1>{{ getTitle() | translate }}</h1> <div>
<p>{{ getSubtitle() | translate }}</p> <h1>{{ getTitle() | translate }}</h1>
<p>{{ getSubtitle() | translate }}</p>
</div>
<button mat-stroked-button color="primary" (click)="navigateToInstalledItems()">
<mat-icon>inventory_2</mat-icon>
{{ 'iot-hub.installed-items' | translate }}
</button>
</div> </div>
} }
@ -187,6 +193,7 @@
[item]="item" [item]="item"
[showTypeChip]="false" [showTypeChip]="false"
[showCreator]="!creatorId" [showCreator]="!creatorId"
[installed]="isInstalled(item)"
(cardClick)="openItemDetail($event)" (cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)" (creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"> (installClick)="installItem($event)">

3
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss

@ -47,6 +47,9 @@
} }
.tb-iot-hub-browse-header { .tb-iot-hub-browse-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px; margin-bottom: 24px;
h1 { h1 {

37
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts

@ -27,6 +27,7 @@ import {
} from '@shared/models/iot-hub/iot-hub-item.models'; } from '@shared/models/iot-hub/iot-hub-item.models';
import { cfTypeTranslations, widgetTypeTranslations, ruleChainTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; import { cfTypeTranslations, widgetTypeTranslations, ruleChainTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service'; 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 { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -89,6 +90,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
widgetTypes: Map<string, string> = widgetTypeTranslations; widgetTypes: Map<string, string> = widgetTypeTranslations;
ruleChainTypes: Map<string, string> = ruleChainTypeTranslations; ruleChainTypes: Map<string, string> = ruleChainTypeTranslations;
installedItemsMap = new Map<string, IotHubInstalledItemInfo>();
private searchSubject = new Subject<string>(); private searchSubject = new Subject<string>();
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -108,6 +111,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
this.loadItems(); this.loadItems();
}); });
this.updateCategories(); this.updateCategories();
this.loadInstalledItemInfos();
this.loadItems(); this.loadItems();
} }
@ -322,31 +326,58 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
} }
} }
isInstalled(item: MpItemVersionView): boolean {
return this.installedItemsMap.has(item.itemId);
}
openItemDetail(item: MpItemVersionView): void { openItemDetail(item: MpItemVersionView): void {
this.dialog.open(TbIotHubItemDetailDialogComponent, { const installedInfo = this.installedItemsMap.get(item.itemId);
const dialogRef = this.dialog.open(TbIotHubItemDetailDialogComponent, {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
autoFocus: false, autoFocus: false,
data: { data: {
item, item,
iotHubApiService: this.iotHubApiService iotHubApiService: this.iotHubApiService,
installedItemInfo: installedInfo
} as IotHubItemDetailDialogData } as IotHubItemDetailDialogData
}); });
dialogRef.afterClosed().subscribe(result => {
if (result === 'installed') {
this.loadInstalledItemInfos();
}
});
} }
installItem(item: MpItemVersionView): void { installItem(item: MpItemVersionView): void {
this.dialog.open(TbIotHubInstallDialogComponent, { const dialogRef = this.dialog.open(TbIotHubInstallDialogComponent, {
panelClass: ['tb-dialog'], panelClass: ['tb-dialog'],
data: { data: {
item, item,
iotHubApiService: this.iotHubApiService iotHubApiService: this.iotHubApiService
} as IotHubInstallDialogData } as IotHubInstallDialogData
}); });
dialogRef.afterClosed().subscribe(result => {
if (result === 'installed') {
this.loadInstalledItemInfos();
}
});
} }
navigateToCreator(creatorId: string): void { navigateToCreator(creatorId: string): void {
this.router.navigate(['/iot-hub/creator', creatorId]); this.router.navigate(['/iot-hub/creator', creatorId]);
} }
navigateToInstalledItems(): void {
this.router.navigate(['/iot-hub/installed']);
}
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 { private updateCategories(): void {
this.categories = getCategoriesForType(this.activeType); this.categories = getCategoriesForType(this.activeType);
} }

25
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts

@ -97,14 +97,23 @@ export class TbIotHubInstallDialogComponent {
this.state = 'installing'; this.state = 'installing';
const versionId = this.item.id as string; const versionId = this.item.id as string;
this.data.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }).subscribe({ this.data.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }).subscribe({
next: () => { next: (result) => {
this.state = 'success'; if (result.success) {
this.store.dispatch(new ActionNotificationShow({ this.state = 'success';
message: this.translate.instant('iot-hub.install-success', { name: this.item.name }), this.store.dispatch(new ActionNotificationShow({
type: 'success', message: this.translate.instant('iot-hub.install-success', { name: this.item.name }),
duration: 3000 type: 'success',
})); duration: 3000
setTimeout(() => this.dialogRef.close(true), 1500); }));
setTimeout(() => this.dialogRef.close('installed'), 1500);
} else {
this.state = 'confirm';
this.store.dispatch(new ActionNotificationShow({
message: result.errorMessage || this.translate.instant('iot-hub.install-error', { name: this.item.name }),
type: 'error',
duration: 5000
}));
}
}, },
error: () => { error: () => {
this.state = 'confirm'; this.state = 'confirm';

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

@ -0,0 +1,116 @@
<!--
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.
-->
<div class="tb-iot-hub-installed-items-container">
<a class="tb-iot-hub-back-link" (click)="navigateToMarketplace()">
<mat-icon>arrow_back</mat-icon>
{{ 'iot-hub.back-to-marketplace' | translate }}
</a>
<div class="tb-iot-hub-installed-items-header">
<h2>{{ 'iot-hub.installed-items' | translate }}</h2>
</div>
<div class="tb-iot-hub-installed-search-row">
<mat-form-field appearance="outline" class="tb-iot-hub-installed-search-field">
<mat-icon matPrefix>search</mat-icon>
<input matInput
[placeholder]="'iot-hub.search-installed-items' | translate"
[value]="textSearch"
(input)="onSearchChange($any($event.target).value)">
@if (textSearch) {
<button matSuffix mat-icon-button (click)="onSearchChange('')">
<mat-icon>close</mat-icon>
</button>
}
</mat-form-field>
</div>
<div class="tb-iot-hub-installed-table-container">
@if (isLoading) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
}
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="itemName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'iot-hub.item-name' | translate }}</th>
<td mat-cell *matCellDef="let item">{{ item.itemName }}</td>
</ng-container>
<ng-container matColumnDef="itemType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'iot-hub.item-type' | translate }}</th>
<td mat-cell *matCellDef="let item">
<span class="tb-iot-hub-type-chip" [ngClass]="getItemTypeChipClass(item.itemType)">{{ getItemTypeLabel(item.itemType) }}</span>
</td>
</ng-container>
<ng-container matColumnDef="version">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'iot-hub.item-version' | translate }}</th>
<td mat-cell *matCellDef="let item">{{ item.version }}</td>
</ng-container>
<ng-container matColumnDef="createdTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'iot-hub.installed-date' | translate }}</th>
<td mat-cell *matCellDef="let item">{{ item.createdTime | date:'medium' }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<button mat-icon-button
[matTooltip]="'iot-hub.view-item-details' | translate"
matTooltipPosition="above"
(click)="viewItemDetails(item); $event.stopPropagation()">
<mat-icon>info_outline</mat-icon>
</button>
@if (getEntityId(item)) {
<button mat-icon-button color="primary"
[matTooltip]="'iot-hub.open-installed-entity' | translate"
matTooltipPosition="above"
(click)="openEntity(item); $event.stopPropagation()">
<mat-icon>open_in_new</mat-icon>
</button>
}
<button mat-icon-button color="warn"
[matTooltip]="'action.delete' | translate"
matTooltipPosition="above"
(click)="deleteItem(item); $event.stopPropagation()">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" [attr.colspan]="displayedColumns.length">
<div class="tb-iot-hub-installed-empty">
{{ 'iot-hub.no-installed-items' | translate }}
</div>
</td>
</tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageIndex]="pageIndex"
[pageSizeOptions]="[10, 20, 50]"
(page)="onPageChange($event)">
</mat-paginator>
</div>
</div>

202
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss

@ -0,0 +1,202 @@
/**
* 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.
*/
:host {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px 24px;
overflow: auto;
}
.tb-iot-hub-installed-items-container {
max-width: 1400px;
margin: 0 auto;
width: 100%;
background: white;
padding: 24px;
border-radius: 4px;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.14), 0 1px 1px rgba(0, 0, 0, 0.12);
}
.tb-iot-hub-back-link {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: rgba(0, 0, 0, 0.54);
cursor: pointer;
text-decoration: none;
border-bottom: none;
margin-bottom: 12px;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
&:hover {
color: rgba(0, 0, 0, 0.87);
text-decoration: none;
border-bottom: none;
}
}
.tb-iot-hub-installed-items-header {
margin-bottom: 24px;
h2 {
font-size: 28px;
font-weight: 700;
margin: 0;
color: rgba(0, 0, 0, 0.87);
}
}
.tb-iot-hub-installed-search-row {
display: flex;
gap: 12px;
margin-bottom: 16px;
align-items: center;
}
.tb-iot-hub-installed-search-field {
flex: 1;
::ng-deep {
.mat-mdc-text-field-wrapper {
height: 42px;
padding: 0 12px;
}
.mat-mdc-form-field-flex {
height: 42px;
align-items: center;
}
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: #d5dbe3;
}
.mdc-notched-outline__leading {
border-radius: 8px 0 0 8px;
}
.mdc-notched-outline__trailing {
border-radius: 0 8px 8px 0;
}
.mat-mdc-form-field-infix {
padding-top: 0;
padding-bottom: 0;
min-height: unset;
}
.mdc-notched-outline__notch {
display: none;
}
.mat-mdc-floating-label {
display: none;
}
.mat-mdc-form-field-subscript-wrapper {
display: none;
}
input.mat-mdc-input-element {
font-size: 14px;
color: #3e4a59;
&::placeholder {
color: #8b95a2;
opacity: 1;
}
}
.mat-icon {
color: #8b95a2;
font-size: 20px;
width: 20px;
height: 20px;
}
.mat-mdc-form-field-icon-prefix {
padding: 0 8px 0 0;
> .mat-icon {
padding: 0;
}
}
.mat-mdc-form-field-icon-suffix {
padding: 0;
.mat-mdc-icon-button {
width: 32px;
height: 32px;
padding: 4px;
}
}
}
&.mat-focused ::ng-deep {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: #8b95a2;
border-width: 1px;
}
}
}
.tb-iot-hub-installed-table-container {
border-radius: 4px;
overflow: hidden;
table {
width: 100%;
}
.mat-column-actions {
text-align: right;
white-space: nowrap;
}
}
.tb-iot-hub-type-chip {
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
&.tb-type-widget { background: #e3f2fd; color: #1565c0; }
&.tb-type-dashboard { background: #e8f5e9; color: #2e7d32; }
&.tb-type-calc-field { background: #fff3e0; color: #e65100; }
&.tb-type-rule-chain { background: #f3e5f5; color: #7b1fa2; }
&.tb-type-device { background: #eceff1; color: #455a64; }
}
.tb-iot-hub-installed-empty {
padding: 48px 24px;
text-align: center;
color: rgba(0, 0, 0, 0.54);
font-size: 14px;
}

210
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts

@ -0,0 +1,210 @@
///
/// 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, OnInit, AfterViewInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { MatSort } from '@angular/material/sort';
import { MatPaginator } from '@angular/material/paginator';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
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 { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { IotHubInstalledItem } 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';
import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData } from './iot-hub-item-detail-dialog.component';
@Component({
selector: 'tb-iot-hub-installed-items',
standalone: false,
templateUrl: './iot-hub-installed-items.component.html',
styleUrls: ['./iot-hub-installed-items.component.scss']
})
export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit {
displayedColumns: string[] = ['itemName', 'itemType', 'version', 'createdTime', 'actions'];
dataSource: IotHubInstalledItem[] = [];
totalElements = 0;
pageSize = 10;
pageIndex = 0;
isLoading = false;
textSearch = '';
private searchSubject = new Subject<string>();
@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
) {}
ngOnInit(): void {
this.searchSubject.pipe(
debounceTime(300)
).subscribe(() => {
this.pageIndex = 0;
this.paginator.pageIndex = 0;
this.loadData();
});
this.loadData();
}
ngAfterViewInit(): void {
this.sort.sortChange.subscribe(() => {
this.pageIndex = 0;
this.paginator.pageIndex = 0;
this.loadData();
});
}
navigateToMarketplace(): void {
this.router.navigate(['/iot-hub']);
}
onSearchChange(value: string): void {
this.textSearch = value;
this.searchSubject.next(value);
}
onPageChange(event: any): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadData();
}
deleteItem(item: IotHubInstalledItem): void {
this.dialogService.confirm(
this.translate.instant('iot-hub.delete-installed-item-title'),
this.translate.instant('iot-hub.delete-installed-item-text', {name: item.itemName}),
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe(result => {
if (result) {
this.iotHubApiService.deleteInstalledItem(item.itemId).subscribe({
next: () => {
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('iot-hub.installed-item-deleted', {name: item.itemName}),
type: 'success',
duration: 3000
}));
this.loadData();
},
error: () => {
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('iot-hub.installed-item-delete-error', {name: item.itemName}),
type: 'error',
duration: 5000
}));
}
});
}
});
}
getItemTypeLabel(itemType: string): string {
const key = itemTypeTranslations.get(itemType as ItemType);
return key ? this.translate.instant(key) : itemType;
}
getItemTypeChipClass(itemType: string): string {
switch (itemType) {
case 'WIDGET': return 'tb-type-widget';
case 'DASHBOARD': return 'tb-type-dashboard';
case 'CALCULATED_FIELD': return 'tb-type-calc-field';
case 'RULE_CHAIN': return 'tb-type-rule-chain';
case 'DEVICE': return 'tb-type-device';
default: return '';
}
}
viewItemDetails(item: IotHubInstalledItem): void {
this.iotHubApiService.getVersionInfo(item.itemVersionId, {ignoreLoading: true}).subscribe(versionView => {
this.dialog.open(TbIotHubItemDetailDialogComponent, {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
item: versionView,
iotHubApiService: this.iotHubApiService,
installedDescriptor: item.descriptor
} as IotHubItemDetailDialogData
});
});
}
getEntityId(item: IotHubInstalledItem): string | null {
const descriptor = item.descriptor;
switch (descriptor.type) {
case 'WIDGET': return descriptor.widgetTypeId?.id;
case 'DASHBOARD': return descriptor.dashboardId?.id;
case 'CALCULATED_FIELD': return descriptor.calculatedFieldId?.id;
case 'RULE_CHAIN': return descriptor.ruleChainId?.id;
default: return null;
}
}
openEntity(item: IotHubInstalledItem): void {
const entityType = TbIotHubInstalledItemsComponent.ITEM_TYPE_TO_ENTITY_TYPE[item.itemType];
const entityId = this.getEntityId(item);
if (entityType && entityId) {
const url = getEntityDetailsPageURL(entityId, entityType);
if (url) {
this.router.navigateByUrl(url);
}
}
}
private loadData(): void {
this.isLoading = true;
const sortOrder: SortOrder = {
property: this.sort?.active || 'createdTime',
direction: this.sort?.direction === 'asc' ? Direction.ASC : Direction.DESC
};
const pageLink = new PageLink(this.pageSize, this.pageIndex, this.textSearch || null, sortOrder);
this.iotHubApiService.getInstalledItems(pageLink, {ignoreLoading: true}).subscribe({
next: (data) => {
this.dataSource = data.data;
this.totalElements = data.totalElements;
this.isLoading = false;
},
error: () => {
this.dataSource = [];
this.totalElements = 0;
this.isLoading = false;
}
});
}
}

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

@ -100,7 +100,11 @@
<tb-icon>download</tb-icon> <tb-icon>download</tb-icon>
{{ item.totalInstallCount | shortNumber }} {{ item.totalInstallCount | shortNumber }}
</span> </span>
<button class="tb-iot-hub-card-install-btn" (click)="onInstallClick($event)">Install</button> @if (installed) {
<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>
}
</span> </span>
</div> </div>
</div> </div>

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

@ -348,4 +348,14 @@
color: #fff; color: #fff;
} }
} }
.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;
}
} }

1
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts

@ -35,6 +35,7 @@ export class TbIotHubItemCardComponent {
@Input() item: MpItemVersionView; @Input() item: MpItemVersionView;
@Input() showCreator = true; @Input() showCreator = true;
@Input() showTypeChip = true; @Input() showTypeChip = true;
@Input() installed = false;
@Output() cardClick = new EventEmitter<MpItemVersionView>(); @Output() cardClick = new EventEmitter<MpItemVersionView>();
@Output() creatorClick = new EventEmitter<string>(); @Output() creatorClick = new EventEmitter<string>();
@Output() installClick = new EventEmitter<MpItemVersionView>(); @Output() installClick = new EventEmitter<MpItemVersionView>();

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

@ -174,9 +174,15 @@
<!-- Footer --> <!-- Footer -->
<div class="dlg-footer"> <div class="dlg-footer">
<button class="dlg-install-btn" (click)="install()"> @if (installedDescriptor) {
{{ 'iot-hub.install-item-type' | translate:{ type: getTypeLabel() } }} <button class="dlg-install-btn" (click)="openEntityDetails()">
</button> {{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }}
</button>
} @else {
<button class="dlg-install-btn" (click)="install()">
{{ 'iot-hub.install-item-type' | translate:{ type: getTypeLabel() } }}
</button>
}
<div class="dlg-creator-info"> <div class="dlg-creator-info">
<span>{{ 'iot-hub.created-by' | translate:{ name: item.creatorDisplayName } }}</span> <span>{{ 'iot-hub.created-by' | translate:{ name: item.creatorDisplayName } }}</span>
<span class="dlg-sep">&middot;</span> <span class="dlg-sep">&middot;</span>

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

@ -19,13 +19,18 @@ import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dial
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, nodeComponentTypeTranslations, NodeInfo } from '@shared/models/iot-hub/iot-hub-version.models'; 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 { 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 { IotHubApiService } from '@core/http/iot-hub-api.service'; import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils';
import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component'; import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component';
export interface IotHubItemDetailDialogData { export interface IotHubItemDetailDialogData {
item: MpItemVersionView; item: MpItemVersionView;
iotHubApiService: IotHubApiService; iotHubApiService: IotHubApiService;
installedDescriptor?: IotHubInstalledItemDescriptor;
installedItemInfo?: IotHubInstalledItemInfo;
} }
@Component({ @Component({
@ -40,6 +45,15 @@ export class TbIotHubItemDetailDialogComponent {
item: MpItemVersionView; item: MpItemVersionView;
typeTranslations = itemTypeTranslations; typeTranslations = itemTypeTranslations;
readmeContent: string = ''; readmeContent: string = '';
installedDescriptor?: IotHubInstalledItemDescriptor;
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
};
private categoryMap: Map<string, string>; private categoryMap: Map<string, string>;
private useCaseMap = useCaseTranslations; private useCaseMap = useCaseTranslations;
@ -52,6 +66,12 @@ export class TbIotHubItemDetailDialogComponent {
private translate: TranslateService private translate: TranslateService
) { ) {
this.item = data.item; this.item = data.item;
this.installedDescriptor = data.installedDescriptor;
if (!this.installedDescriptor && data.installedItemInfo) {
this.data.iotHubApiService.getInstalledItemByItemId(data.installedItemInfo.itemId, {ignoreLoading: true}).subscribe(
installedItem => this.installedDescriptor = installedItem?.descriptor
);
}
this.categoryMap = getCategoriesForType(this.item.type); this.categoryMap = getCategoriesForType(this.item.type);
this.loadReadme(); this.loadReadme();
} }
@ -210,13 +230,18 @@ export class TbIotHubItemDetailDialogComponent {
} }
install(): void { install(): void {
this.dialog.open(TbIotHubInstallDialogComponent, { const dialogRef = this.dialog.open(TbIotHubInstallDialogComponent, {
panelClass: ['tb-dialog'], panelClass: ['tb-dialog'],
data: { data: {
item: this.item, item: this.item,
iotHubApiService: this.data.iotHubApiService iotHubApiService: this.data.iotHubApiService
} as IotHubInstallDialogData } as IotHubInstallDialogData
}); });
dialogRef.afterClosed().subscribe(result => {
if (result === 'installed') {
this.dialogRef.close('installed');
}
});
} }
openSignup(): void { openSignup(): void {
@ -228,6 +253,28 @@ export class TbIotHubItemDetailDialogComponent {
this.router.navigate(['/iot-hub/creator', this.item.creatorId]); this.router.navigate(['/iot-hub/creator', this.item.creatorId]);
} }
openEntityDetails(): void {
const descriptor = this.installedDescriptor;
if (!descriptor) {
return;
}
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;
}
const entityType = TbIotHubItemDetailDialogComponent.ITEM_TYPE_TO_ENTITY_TYPE[this.item.type];
if (entityType && entityId) {
const url = getEntityDetailsPageURL(entityId, entityType);
if (url) {
this.dialogRef.close();
this.router.navigateByUrl(url);
}
}
}
close(): void { close(): void {
this.dialogRef.close(); this.dialogRef.close();
} }

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

@ -20,6 +20,7 @@ import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { TbIotHubBrowseComponent } from './iot-hub-browse.component'; import { TbIotHubBrowseComponent } from './iot-hub-browse.component';
import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component';
import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component';
const routes: Routes = [ const routes: Routes = [
{ {
@ -40,6 +41,18 @@ const routes: Routes = [
title: 'iot-hub.browse' title: 'iot-hub.browse'
} }
}, },
{
path: 'installed',
component: TbIotHubInstalledItemsComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'iot-hub.installed-items',
breadcrumb: {
label: 'iot-hub.installed-items',
icon: 'inventory_2'
}
}
},
{ {
path: 'creator/:creatorId', path: 'creator/:creatorId',
component: TbIotHubCreatorProfileComponent, component: TbIotHubCreatorProfileComponent,

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

@ -23,6 +23,7 @@ import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.compo
import { TbIotHubItemDetailDialogComponent } from './iot-hub-item-detail-dialog.component'; import { TbIotHubItemDetailDialogComponent } from './iot-hub-item-detail-dialog.component';
import { TbIotHubInstallDialogComponent } from './iot-hub-install-dialog.component'; import { TbIotHubInstallDialogComponent } from './iot-hub-install-dialog.component';
import { TbIotHubItemCardComponent } from './iot-hub-item-card.component'; import { TbIotHubItemCardComponent } from './iot-hub-item-card.component';
import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -30,7 +31,8 @@ import { TbIotHubItemCardComponent } from './iot-hub-item-card.component';
TbIotHubCreatorProfileComponent, TbIotHubCreatorProfileComponent,
TbIotHubItemDetailDialogComponent, TbIotHubItemDetailDialogComponent,
TbIotHubInstallDialogComponent, TbIotHubInstallDialogComponent,
TbIotHubItemCardComponent TbIotHubItemCardComponent,
TbIotHubInstalledItemsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

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

@ -0,0 +1,63 @@
///
/// 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 { BaseData } from '@shared/models/base-data';
export interface WidgetInstalledItemDescriptor {
type: 'WIDGET';
widgetTypeId: { id: string };
}
export interface DashboardInstalledItemDescriptor {
type: 'DASHBOARD';
dashboardId: { id: string };
}
export interface CalculatedFieldInstalledItemDescriptor {
type: 'CALCULATED_FIELD';
calculatedFieldId: { id: string };
}
export interface RuleChainInstalledItemDescriptor {
type: 'RULE_CHAIN';
ruleChainId: { id: string };
}
export interface DeviceInstalledItemDescriptor {
type: 'DEVICE';
}
export type IotHubInstalledItemDescriptor =
| WidgetInstalledItemDescriptor
| DashboardInstalledItemDescriptor
| CalculatedFieldInstalledItemDescriptor
| RuleChainInstalledItemDescriptor
| DeviceInstalledItemDescriptor;
export interface IotHubInstalledItemInfo {
itemId: string;
itemVersionId: string;
}
export interface IotHubInstalledItem extends BaseData<{id: string}> {
tenantId: { id: string };
itemId: string;
itemVersionId: string;
itemName: string;
itemType: string;
version: string;
descriptor: IotHubInstalledItemDescriptor;
}

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

@ -3782,7 +3782,23 @@
"use-case.smart-city": "Smart City", "use-case.smart-city": "Smart City",
"use-case.healthcare": "Healthcare", "use-case.healthcare": "Healthcare",
"use-case.retail": "Retail", "use-case.retail": "Retail",
"use-case.water-utilities": "Water & Utilities" "use-case.water-utilities": "Water & Utilities",
"installed-items": "Installed Items",
"search-installed-items": "Search installed items...",
"item-name": "Name",
"item-type": "Type",
"item-version": "Version",
"installed-date": "Installed",
"actions": "Actions",
"no-installed-items": "No installed items",
"delete-installed-item-title": "Delete Installed Item",
"delete-installed-item-text": "Are you sure you want to delete '{{name}}'? The installed entity will also be removed.",
"installed-item-deleted": "'{{name}}' has been deleted",
"installed-item-delete-error": "Failed to delete '{{name}}'",
"open-installed-entity": "Open installed entity",
"view-item-details": "View item details",
"open-item-type-details": "Open {{type}} details",
"back-to-marketplace": "Back to Marketplace"
}, },
"item-data": { "item-data": {
"widget-type-timeseries": "Timeseries", "widget-type-timeseries": "Timeseries",

Loading…
Cancel
Save