From 1e70c4acad533c9af6e1f55705e2763dbb155e6b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 1 Apr 2026 16:08:15 +0300 Subject: [PATCH] Add installed items count API, installed items button, and menu reorder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add getInstalledItemsCount endpoint through full stack (controller → DAO) with optional itemType filter - Add getInstalledItemsCount to UI IotHubApiService - Home page: show "Installed Items: N" stroked button at top-right when count > 0, white bg, primary border, card shadow; navigates to installed items - Count updates on install/delete via API re-fetch and local decrement - Move IoT Hub menu item right after Dashboards in sidebar --- .../server/controller/IotHubController.java | 7 +++++++ .../dao/iot_hub/IotHubInstalledItemDao.java | 2 ++ .../iot_hub/IotHubInstalledItemService.java | 2 ++ .../IotHubInstalledItemServiceImpl.java | 5 +++++ .../iot_hub/IotHubInstalledItemRepository.java | 7 +++++++ .../sql/iot_hub/JpaIotHubInstalledItemDao.java | 5 +++++ .../src/app/core/http/iot-hub-api.service.ts | 8 ++++++++ .../pages/iot-hub/iot-hub-home.component.html | 8 ++++++++ .../pages/iot-hub/iot-hub-home.component.scss | 18 +++++++++++++++++- .../pages/iot-hub/iot-hub-home.component.ts | 9 ++++++++- 10 files changed, 69 insertions(+), 2 deletions(-) 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 86253a9195..62989a1c3c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java +++ b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java @@ -87,6 +87,13 @@ public class IotHubController extends BaseController { return iotHubInstalledItemService.findByTenantId(getTenantId(), itemType, pageLink); } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/installedItems/count") + @ResponseBody + public long getInstalledItemsCount(@RequestParam(required = false) String itemType) throws ThingsboardException { + return iotHubInstalledItemService.countByTenantId(getTenantId(), itemType); + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping("/installedItems/itemIds") @ResponseBody diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java index e5333c71ef..2a5b88804a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java @@ -30,6 +30,8 @@ public interface IotHubInstalledItemDao extends Dao { List findInstalledItemIdsByTenantId(TenantId tenantId); + long countByTenantId(TenantId tenantId, String itemType); + void deleteByTenantId(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java index 8973292f90..5a9d434c08 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java @@ -34,6 +34,8 @@ public interface IotHubInstalledItemService { List findInstalledItemIdsByTenantId(TenantId tenantId); + long countByTenantId(TenantId tenantId, String itemType); + void deleteById(TenantId tenantId, IotHubInstalledItemId id); void deleteByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java index 3a28a3a01d..28197668da 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java @@ -55,6 +55,11 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService { return iotHubInstalledItemDao.findInstalledItemIdsByTenantId(tenantId); } + @Override + public long countByTenantId(TenantId tenantId, String itemType) { + return iotHubInstalledItemDao.countByTenantId(tenantId, itemType); + } + @Override public void deleteById(TenantId tenantId, IotHubInstalledItemId id) { iotHubInstalledItemDao.removeById(tenantId, id.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java index 84f2da1bf0..3dde0d3b09 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java @@ -43,6 +43,13 @@ interface IotHubInstalledItemRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("itemType") String itemType, @Param("textSearch") String textSearch, Pageable pageable); + @Query(""" + SELECT COUNT(item) FROM IotHubInstalledItemEntity item + WHERE item.tenantId = :tenantId + AND (:itemType IS NULL OR item.itemType = :itemType) + """) + long countByTenantId(@Param("tenantId") UUID tenantId, @Param("itemType") String itemType); + @Transactional @Modifying @Query(value = "DELETE FROM iot_hub_installed_item WHERE tenant_id = :tenantId", nativeQuery = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java index 33a15c91b1..ff2664127b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java @@ -57,6 +57,11 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao { + let url = '/api/iot-hub/installedItems/count'; + if (itemType) { + url += `?itemType=${itemType}`; + } + return this.http.get(url, { params: this.buildParams(config) }); + } + public deleteInstalledItem(installedItemId: string, config?: IotHubRequestConfig): Observable { return this.http.delete( `/api/iot-hub/installedItems/${installedItemId}`, diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html index 301d9c27fb..0d7c9e7fb9 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html @@ -16,6 +16,14 @@ -->
+ + @if (installedItemsCount > 0) { + + } +
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss index d07ade2ad6..11795fa233 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss @@ -14,6 +14,8 @@ * limitations under the License. */ +@import "../../../../../scss/constants"; + // Outer page — 16px padding around the white container :host { display: block; @@ -43,12 +45,26 @@ z-index: 0; } - > * { + > *:not(.tb-iot-hub-installed-btn) { position: relative; z-index: 1; } } +// Installed Items button — Design: top-right, white bg, primary border, card shadow +.tb-iot-hub-installed-btn { + position: absolute; + top: 24px; + right: 24px; + z-index: 2; + background: white; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.08), 0 4px 8px 0 rgba(0, 0, 0, 0.08); + + &.mat-primary { + --mat-button-outlined-outline-color: #{$tb-primary-color}; + } +} + // Hero section .tb-iot-hub-hero { text-align: center; diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts index 7f8e5a77a7..45905ea495 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts @@ -106,6 +106,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { installedWidgets: IotHubInstalledItem[] = []; installedSolutionTemplates: IotHubInstalledItem[] = []; + installedItemsCount = 0; isLoading = true; @@ -187,6 +188,9 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { private reloadInstalledItems(type: ItemType): void { const config = { ignoreLoading: true }; const pageLink = new PageLink(10000, 0); + this.iotHubApiService.getInstalledItemsCount(null, config).subscribe(count => { + this.installedItemsCount = count; + }); if (type === ItemType.WIDGET) { this.iotHubApiService.getInstalledItems(pageLink, ItemType.WIDGET, config).subscribe(data => { this.installedWidgets = data.data; @@ -250,6 +254,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { dialogRef.afterClosed().subscribe(confirmed => { if (confirmed) { this.iotHubApiService.deleteInstalledItem(installedItem.id.id).subscribe(() => { + this.installedItemsCount = Math.max(0, this.installedItemsCount - 1); if (item.type === ItemType.WIDGET) { this.installedWidgets = this.installedWidgets.filter(i => i.id.id !== installedItem.id.id); } else if (item.type === ItemType.SOLUTION_TEMPLATE) { @@ -281,7 +286,8 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { calcFields: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.CALCULATED_FIELD, 6), config), ruleChains: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.RULE_CHAIN, 6), config), installedWidgets: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.WIDGET, config), - installedSolutionTemplates: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.SOLUTION_TEMPLATE, config) + installedSolutionTemplates: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.SOLUTION_TEMPLATE, config), + installedCount: this.iotHubApiService.getInstalledItemsCount(null, config) }).subscribe({ next: (results) => { this.popularWidgets = results.widgets.data; @@ -291,6 +297,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { this.popularRuleChains = results.ruleChains.data; this.installedWidgets = results.installedWidgets.data; this.installedSolutionTemplates = results.installedSolutionTemplates.data; + this.installedItemsCount = results.installedCount; this.isLoading = false; }, error: () => {