From 285c8625536113c93458b7c6a98db8dfcadbc545 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 10 Mar 2026 12:20:45 +0200 Subject: [PATCH] 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 --- .../main/data/upgrade/basic/schema_update.sql | 19 +- .../server/controller/IotHubController.java | 49 +++- .../service/iot_hub/DefaultIotHubService.java | 172 ++++++++++---- .../iot_hub/InstallItemVersionResult.java | 40 ++++ .../server/service/iot_hub/IotHubService.java | 7 +- .../common/data/id/IotHubInstalledItemId.java | 34 +++ ...alculatedFieldInstalledItemDescriptor.java | 26 +++ .../DashboardInstalledItemDescriptor.java | 26 +++ .../DeviceInstalledItemDescriptor.java | 23 ++ .../data/iot_hub/IotHubInstalledItem.java | 47 ++++ .../IotHubInstalledItemDescriptor.java | 38 ++++ .../data/iot_hub/IotHubInstalledItemInfo.java | 32 +++ .../RuleChainInstalledItemDescriptor.java | 26 +++ .../WidgetInstalledItemDescriptor.java | 26 +++ .../dao/iot_hub/IotHubInstalledItemDao.java | 41 ++++ .../iot_hub/IotHubInstalledItemService.java | 42 ++++ .../IotHubInstalledItemServiceImpl.java | 69 ++++++ .../server/dao/model/ModelConstants.java | 12 + .../model/sql/IotHubInstalledItemEntity.java | 110 +++++++++ .../IotHubInstalledItemRepository.java | 60 +++++ .../iot_hub/JpaIotHubInstalledItemDao.java | 100 +++++++++ .../server/dao/tenant/TenantServiceImpl.java | 3 + .../main/resources/sql/schema-entities.sql | 13 ++ .../src/app/core/http/iot-hub-api.service.ts | 37 +++ .../iot-hub/iot-hub-browse.component.html | 11 +- .../iot-hub/iot-hub-browse.component.scss | 3 + .../pages/iot-hub/iot-hub-browse.component.ts | 37 ++- .../iot-hub-install-dialog.component.ts | 25 ++- .../iot-hub-installed-items.component.html | 116 ++++++++++ .../iot-hub-installed-items.component.scss | 202 +++++++++++++++++ .../iot-hub-installed-items.component.ts | 210 ++++++++++++++++++ .../iot-hub/iot-hub-item-card.component.html | 6 +- .../iot-hub/iot-hub-item-card.component.scss | 10 + .../iot-hub/iot-hub-item-card.component.ts | 1 + .../iot-hub-item-detail-dialog.component.html | 12 +- .../iot-hub-item-detail-dialog.component.ts | 49 +++- .../pages/iot-hub/iot-hub-routing.module.ts | 13 ++ .../home/pages/iot-hub/iot-hub.module.ts | 4 +- .../iot-hub/iot-hub-installed-item.models.ts | 63 ++++++ .../assets/locale/locale.constant-en_US.json | 18 +- 40 files changed, 1763 insertions(+), 69 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/iot_hub/InstallItemVersionResult.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/IotHubInstalledItemId.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DashboardInstalledItemDescriptor.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItem.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemInfo.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/WidgetInstalledItemDescriptor.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts create mode 100644 ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 98c7a93c52..3df3837944 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/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 -ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255); \ No newline at end of file +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 \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java index eef17d8c96..e2b6b297ab 100644 --- a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java +++ b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java @@ -15,18 +15,29 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Hidden; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +import java.util.List; +import java.util.UUID; +import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.iot_hub.InstallItemVersionResult; import org.thingsboard.server.service.iot_hub.IotHubService; @Hidden @@ -38,11 +49,45 @@ import org.thingsboard.server.service.iot_hub.IotHubService; public class IotHubController extends BaseController { private final IotHubService iotHubService; + private final IotHubInstalledItemService iotHubInstalledItemService; @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/versions/{versionId}/install") @ResponseBody - public JsonNode installItemVersion(@PathVariable String versionId) throws Exception { + public InstallItemVersionResult installItemVersion(@PathVariable String versionId) throws ThingsboardException { return iotHubService.installItemVersion(getCurrentUser(), versionId); } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/installedItems") + @ResponseBody + public PageData 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 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); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java index 173ae79020..bef8cfdd10 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -24,8 +24,23 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.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.RuleChainImportResult; 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.service.entitiy.cf.TbCalculatedFieldService; 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.rule.TbRuleChainService; import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.dao.rule.RuleChainService; + +import java.util.List; +import java.util.UUID; @Service @TbCoreComponent @@ -48,81 +65,148 @@ public class DefaultIotHubService implements IotHubService { private final RuleChainService ruleChainService; private final TbRuleChainService tbRuleChainService; 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 - public JsonNode installItemVersion(SecurityUser user, String versionId) throws Exception { + public InstallItemVersionResult installItemVersion(SecurityUser user, String versionId) { TenantId tenantId = user.getTenantId(); log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId); - JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); - String itemType = versionInfo.get("type").asText(); - String itemName = versionInfo.get("name").asText(); - log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType); - - byte[] fileData = iotHubRestClient.getVersionFileData(versionId); - log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0); - - switch (itemType) { - case "WIDGET": - 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); - } + try { + JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); + String itemType = versionInfo.get("type").asText(); + String itemName = versionInfo.get("name").asText(); + UUID itemId = UUID.fromString(versionInfo.get("itemId").asText()); + String version = versionInfo.get("version").asText(); + log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType); + + if (iotHubInstalledItemService.findByTenantIdAndItemId(tenantId, itemId).isPresent()) { + return InstallItemVersionResult.error("Item '" + itemName + "' is already installed"); + } - iotHubRestClient.reportVersionInstalled(versionId); - log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType); + byte[] fileData = iotHubRestClient.getVersionFileData(versionId); + 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.setId(null); widgetTypeDetails.setTenantId(tenantId); - tbWidgetTypeService.save(widgetTypeDetails, true, user); - log.debug("[{}] Widget installed: {}", tenantId, widgetTypeDetails.getName()); + WidgetTypeDetails saved = tbWidgetTypeService.save(widgetTypeDetails, true, user); + 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.setId(null); dashboard.setTenantId(tenantId); - tbDashboardService.save(dashboard, user); - log.debug("[{}] Dashboard installed: {}", tenantId, dashboard.getTitle()); + Dashboard saved = tbDashboardService.save(dashboard, user); + 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.setId(null); calculatedField.setTenantId(tenantId); - tbCalculatedFieldService.save(calculatedField, user); - log.debug("[{}] Calculated field installed: {}", tenantId, calculatedField.getName()); + CalculatedField saved = tbCalculatedFieldService.save(calculatedField, user); + 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); - ruleChainService.importTenantRuleChains(tenantId, ruleChainData, false, tbRuleChainService::updateRuleNodeConfiguration); + List results = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, false, tbRuleChainService::updateRuleNodeConfiguration); 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.setId(null); deviceProfile.setTenantId(tenantId); tbDeviceProfileService.save(deviceProfile, user); 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()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallItemVersionResult.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallItemVersionResult.java new file mode 100644 index 0000000000..f91085cfd6 --- /dev/null +++ b/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); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java index 9a92825d82..f0cfc6c49d 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java @@ -15,10 +15,13 @@ */ package org.thingsboard.server.service.iot_hub; -import com.fasterxml.jackson.databind.JsonNode; import org.thingsboard.server.service.security.model.SecurityUser; +import java.util.UUID; + public interface IotHubService { - JsonNode installItemVersion(SecurityUser user, String versionId) throws Exception; + InstallItemVersionResult installItemVersion(SecurityUser user, String versionId); + + void deleteInstalledItem(SecurityUser user, UUID itemId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/IotHubInstalledItemId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/IotHubInstalledItemId.java new file mode 100644 index 0000000000..f9ea6c4443 --- /dev/null +++ b/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)); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java new file mode 100644 index 0000000000..b47b9f8cc8 --- /dev/null +++ b/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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DashboardInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DashboardInstalledItemDescriptor.java new file mode 100644 index 0000000000..260665b82f --- /dev/null +++ b/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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java new file mode 100644 index 0000000000..b558058db5 --- /dev/null +++ b/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 { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItem.java new file mode 100644 index 0000000000..8152ec1b0d --- /dev/null +++ b/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 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); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java new file mode 100644 index 0000000000..e08b686515 --- /dev/null +++ b/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 { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemInfo.java new file mode 100644 index 0000000000..19eb74dccb --- /dev/null +++ b/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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java new file mode 100644 index 0000000000..f684607759 --- /dev/null +++ b/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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/WidgetInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/WidgetInstalledItemDescriptor.java new file mode 100644 index 0000000000..5c4195cb51 --- /dev/null +++ b/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; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java new file mode 100644 index 0000000000..300b1cd620 --- /dev/null +++ b/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 { + + Optional findByTenantIdAndItemId(TenantId tenantId, UUID itemId); + + PageData findByTenantId(TenantId tenantId, PageLink pageLink); + + List findInstalledItemInfosByTenantId(TenantId tenantId); + + boolean deleteByTenantIdAndItemId(TenantId tenantId, UUID itemId); + + void deleteByTenantId(TenantId tenantId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java new file mode 100644 index 0000000000..1cee8f10a2 --- /dev/null +++ b/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 findByTenantIdAndItemId(TenantId tenantId, UUID itemId); + + PageData findByTenantId(TenantId tenantId, PageLink pageLink); + + List findInstalledItemInfosByTenantId(TenantId tenantId); + + boolean deleteByTenantIdAndItemId(TenantId tenantId, UUID itemId); + + void deleteByTenantId(TenantId tenantId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java new file mode 100644 index 0000000000..37593df30b --- /dev/null +++ b/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 findByTenantIdAndItemId(TenantId tenantId, UUID itemId) { + return iotHubInstalledItemDao.findByTenantIdAndItemId(tenantId, itemId); + } + + @Override + public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { + return iotHubInstalledItemDao.findByTenantId(tenantId, pageLink); + } + + @Override + public List 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); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index de1a04e81e..d0573d0b69 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/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_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[] 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)}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java new file mode 100644 index 0000000000..e0cd2f1c17 --- /dev/null +++ b/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 { + + @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(); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java new file mode 100644 index 0000000000..fa4b98da6d --- /dev/null +++ b/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 { + + Optional findByTenantIdAndItemId(UUID tenantId, UUID itemId); + + @Query("SELECT new org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemInfo(item.itemId, item.itemVersionId) " + + "FROM IotHubInstalledItemEntity item WHERE item.tenantId = :tenantId") + List findInstalledItemInfosByTenantId(@Param("tenantId") UUID tenantId); + + @Query(""" + SELECT 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 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); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java new file mode 100644 index 0000000000..265026d710 --- /dev/null +++ b/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 implements IotHubInstalledItemDao { + + private final IotHubInstalledItemRepository repository; + + @Override + public Optional findByTenantIdAndItemId(TenantId tenantId, UUID itemId) { + return repository.findByTenantIdAndItemId(tenantId.getId(), itemId).map(DaoUtil::getData); + } + + @Override + public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(repository.findByTenantId( + 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 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 getEntityClass() { + return IotHubInstalledItemEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return repository; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 3674804493..e7939ea32e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -83,6 +83,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService existsTenantCache; @@ -168,6 +170,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService { + return this.http.get( + `${this.baseUrl}/api/versions/${versionId}`, + { params: this.buildParams(config) } + ); + } + public getVersionReadme(versionId: string, config?: IotHubRequestConfig): Observable { return this.http.get(`${this.baseUrl}/api/versions/${versionId}/readme`, { params: this.buildParams(config), @@ -111,6 +120,34 @@ export class IotHubApiService { ); } + public getInstalledItemByItemId(itemId: string, config?: IotHubRequestConfig): Observable { + return this.http.get( + `/api/iot-hub/installedItems/byItemId/${itemId}`, + { params: this.buildParams(config) } + ); + } + + public getInstalledItemInfos(config?: IotHubRequestConfig): Observable { + return this.http.get( + `/api/iot-hub/installedItems/info`, + { params: this.buildParams(config) } + ); + } + + public getInstalledItems(pageLink: PageLink, config?: IotHubRequestConfig): Observable> { + return this.http.get>( + `/api/iot-hub/installedItems${pageLink.toQuery()}`, + { params: this.buildParams(config) } + ); + } + + public deleteInstalledItem(itemId: string, config?: IotHubRequestConfig): Observable { + return this.http.delete( + `/api/iot-hub/installedItems/${itemId}`, + { params: this.buildParams(config) } + ); + } + public getCreatorProfile(creatorId: string, config?: IotHubRequestConfig): Observable { return this.http.get( `${this.baseUrl}/api/creators/${creatorId}/profile`, diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html index b9f4bf4d75..83cf44e087 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html @@ -19,8 +19,14 @@ @if (!embedded) {
-

{{ getTitle() | translate }}

-

{{ getSubtitle() | translate }}

+
+

{{ getTitle() | translate }}

+

{{ getSubtitle() | translate }}

+
+
} @@ -187,6 +193,7 @@ [item]="item" [showTypeChip]="false" [showCreator]="!creatorId" + [installed]="isInstalled(item)" (cardClick)="openItemDetail($event)" (creatorClick)="navigateToCreator($event)" (installClick)="installItem($event)"> diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss index 110133e5ff..deef2d98bc 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss @@ -47,6 +47,9 @@ } .tb-iot-hub-browse-header { + display: flex; + justify-content: space-between; + align-items: flex-start; margin-bottom: 24px; h1 { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts index 517e91ef8c..004dac5fde 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts @@ -27,6 +27,7 @@ import { } from '@shared/models/iot-hub/iot-hub-item.models'; import { cfTypeTranslations, widgetTypeTranslations, ruleChainTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; +import { IotHubInstalledItemInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { MatDialog } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { Router } from '@angular/router'; @@ -89,6 +90,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { widgetTypes: Map = widgetTypeTranslations; ruleChainTypes: Map = ruleChainTypeTranslations; + installedItemsMap = new Map(); + private searchSubject = new Subject(); private destroy$ = new Subject(); @@ -108,6 +111,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); }); this.updateCategories(); + this.loadInstalledItemInfos(); 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 { - this.dialog.open(TbIotHubItemDetailDialogComponent, { + const installedInfo = this.installedItemsMap.get(item.itemId); + const dialogRef = this.dialog.open(TbIotHubItemDetailDialogComponent, { panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], autoFocus: false, data: { item, - iotHubApiService: this.iotHubApiService + iotHubApiService: this.iotHubApiService, + installedItemInfo: installedInfo } as IotHubItemDetailDialogData }); + dialogRef.afterClosed().subscribe(result => { + if (result === 'installed') { + this.loadInstalledItemInfos(); + } + }); } installItem(item: MpItemVersionView): void { - this.dialog.open(TbIotHubInstallDialogComponent, { + const dialogRef = this.dialog.open(TbIotHubInstallDialogComponent, { panelClass: ['tb-dialog'], data: { item, iotHubApiService: this.iotHubApiService } as IotHubInstallDialogData }); + dialogRef.afterClosed().subscribe(result => { + if (result === 'installed') { + this.loadInstalledItemInfos(); + } + }); } navigateToCreator(creatorId: string): void { 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 { this.categories = getCategoriesForType(this.activeType); } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts index f8132ec5c5..0fe5dd867b 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts @@ -97,14 +97,23 @@ export class TbIotHubInstallDialogComponent { this.state = 'installing'; const versionId = this.item.id as string; this.data.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }).subscribe({ - next: () => { - this.state = 'success'; - this.store.dispatch(new ActionNotificationShow({ - message: this.translate.instant('iot-hub.install-success', { name: this.item.name }), - type: 'success', - duration: 3000 - })); - setTimeout(() => this.dialogRef.close(true), 1500); + next: (result) => { + if (result.success) { + this.state = 'success'; + this.store.dispatch(new ActionNotificationShow({ + message: this.translate.instant('iot-hub.install-success', { name: this.item.name }), + type: 'success', + duration: 3000 + })); + 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: () => { this.state = 'confirm'; diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html new file mode 100644 index 0000000000..b76b8301a8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html @@ -0,0 +1,116 @@ + +
+ + arrow_back + {{ 'iot-hub.back-to-marketplace' | translate }} + +
+

{{ 'iot-hub.installed-items' | translate }}

+
+ +
+ + search + + @if (textSearch) { + + } + +
+ +
+ @if (isLoading) { + + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'iot-hub.item-name' | translate }}{{ item.itemName }}{{ 'iot-hub.item-type' | translate }} + {{ getItemTypeLabel(item.itemType) }} + {{ 'iot-hub.item-version' | translate }}{{ item.version }}{{ 'iot-hub.installed-date' | translate }}{{ item.createdTime | date:'medium' }} + + @if (getEntityId(item)) { + + } + +
+
+ {{ 'iot-hub.no-installed-items' | translate }} +
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss new file mode 100644 index 0000000000..b8d0596781 --- /dev/null +++ b/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; +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts new file mode 100644 index 0000000000..0c2902856f --- /dev/null +++ b/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(); + + @ViewChild(MatSort, {static: true}) sort: MatSort; + @ViewChild(MatPaginator, {static: true}) paginator: MatPaginator; + + private static readonly ITEM_TYPE_TO_ENTITY_TYPE: Record = { + 'WIDGET': EntityType.WIDGET_TYPE, + 'DASHBOARD': EntityType.DASHBOARD, + 'CALCULATED_FIELD': EntityType.CALCULATED_FIELD, + 'RULE_CHAIN': EntityType.RULE_CHAIN, + 'DEVICE': EntityType.DEVICE_PROFILE + }; + + constructor( + private iotHubApiService: IotHubApiService, + private dialogService: DialogService, + private translate: TranslateService, + private store: Store, + private router: Router, + private dialog: MatDialog + ) {} + + 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; + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html index 8b99b4cbe0..3eb8620633 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html @@ -100,7 +100,11 @@ download {{ item.totalInstallCount | shortNumber }} - + @if (installed) { + {{ 'iot-hub.installed' | translate }} + } @else { + + } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss index 0f16dd4fae..20f1d873b9 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss @@ -348,4 +348,14 @@ 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; + } } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts index 87f5502696..709559e534 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts @@ -35,6 +35,7 @@ export class TbIotHubItemCardComponent { @Input() item: MpItemVersionView; @Input() showCreator = true; @Input() showTypeChip = true; + @Input() installed = false; @Output() cardClick = new EventEmitter(); @Output() creatorClick = new EventEmitter(); @Output() installClick = new EventEmitter(); diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html index 6d30c3c920..f4746d4b63 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html @@ -174,9 +174,15 @@