From 8e8efed94cdaa42f098252aeb192b4ab4c2fb982 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 17 Apr 2026 19:43:39 +0300 Subject: [PATCH] feat(iot-hub): redesign item detail dialog, add installed item counts API Redesign item detail dialog: move install/add/update/open buttons to footer, move creator to sticky meta bar with avatar and verified badge, add manage section with solution instructions, remove, and installed items count buttons, update subtitle with grouped icon+text pairs matching design. Add getInstalledItemCounts API (backend + frontend) returning item counts by itemId per type. Replace installedDevices list with count-based tracking in home, browse, and search components. Add installedItemsCount to item card and detail dialog data flow. --- .../server/controller/IotHubController.java | 8 + .../dao/iot_hub/IotHubInstalledItemDao.java | 3 + .../iot_hub/IotHubInstalledItemService.java | 3 + .../IotHubInstalledItemServiceImpl.java | 6 + .../IotHubInstalledItemRepository.java | 8 + .../iot_hub/JpaIotHubInstalledItemDao.java | 12 + .../src/app/core/http/iot-hub-api.service.ts | 7 + .../iot-hub/iot-hub-actions.service.ts | 4 +- .../iot-hub/iot-hub-browse.component.html | 1 + .../iot-hub/iot-hub-browse.component.ts | 40 +-- .../iot-hub/iot-hub-item-card.component.ts | 1 + .../iot-hub-item-detail-dialog.component.html | 240 +++++++++--------- .../iot-hub-item-detail-dialog.component.scss | 136 +++++++++- .../iot-hub-item-detail-dialog.component.ts | 22 +- .../iot-hub/iot-hub-search.component.html | 1 + .../iot-hub/iot-hub-search.component.ts | 31 ++- .../pages/iot-hub/iot-hub-home.component.html | 6 +- .../pages/iot-hub/iot-hub-home.component.ts | 64 ++++- .../assets/locale/locale.constant-en_US.json | 4 + 19 files changed, 414 insertions(+), 183 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 abe05bc2cf..39eea5852b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java +++ b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import java.util.List; +import java.util.Map; import java.util.UUID; import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.dao.device.DeviceConnectivityService; @@ -117,6 +118,13 @@ public class IotHubController extends BaseController { return iotHubInstalledItemService.findInstalledItemIdsByTenantId(getTenantId()); } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/installedItems/counts") + @ResponseBody + public Map getInstalledItemCounts(@RequestParam String itemType) throws ThingsboardException { + return iotHubInstalledItemService.findInstalledItemCounts(getTenantId(), itemType); + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @DeleteMapping("/installedItems/{installedItemId}") @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 27af668bbe..e9ff48f9a7 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; +import java.util.Map; import java.util.UUID; public interface IotHubInstalledItemDao extends Dao { @@ -32,6 +33,8 @@ public interface IotHubInstalledItemDao extends Dao { long countByTenantId(TenantId tenantId, String itemType); + Map findInstalledItemCounts(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 71176f09e0..3374554e54 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import java.util.List; +import java.util.Map; import java.util.UUID; public interface IotHubInstalledItemService { @@ -36,6 +37,8 @@ public interface IotHubInstalledItemService { long countByTenantId(TenantId tenantId, String itemType); + Map findInstalledItemCounts(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 4d49e20584..42cf853b2b 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 @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import java.util.List; +import java.util.Map; import java.util.UUID; @Service @@ -60,6 +61,11 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService { return iotHubInstalledItemDao.countByTenantId(tenantId, itemType); } + @Override + public Map findInstalledItemCounts(TenantId tenantId, String itemType) { + return iotHubInstalledItemDao.findInstalledItemCounts(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 62a562c537..45f6f988ad 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 @@ -50,6 +50,14 @@ interface IotHubInstalledItemRepository extends JpaRepository findInstalledItemCounts(@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 9eb9a068b0..b644e57e41 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 @@ -32,7 +32,9 @@ 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.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; @SqlDao @@ -62,6 +64,16 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao findInstalledItemCounts(TenantId tenantId, String itemType) { + List results = repository.findInstalledItemCounts(tenantId.getId(), itemType); + Map counts = new HashMap<>(); + for (Object[] row : results) { + counts.put((UUID) row[0], (Long) row[1]); + } + return counts; + } + @Override public void deleteByTenantId(TenantId tenantId) { repository.deleteByTenantId(tenantId.getId()); diff --git a/ui-ngx/src/app/core/http/iot-hub-api.service.ts b/ui-ngx/src/app/core/http/iot-hub-api.service.ts index dbe6e98706..6292f17fcf 100644 --- a/ui-ngx/src/app/core/http/iot-hub-api.service.ts +++ b/ui-ngx/src/app/core/http/iot-hub-api.service.ts @@ -167,6 +167,13 @@ export class IotHubApiService { ); } + public getInstalledItemCounts(itemType: string, config?: IotHubRequestConfig): Observable> { + return this.http.get>( + `/api/iot-hub/installedItems/counts?itemType=${itemType}`, + { params: this.buildParams(config) } + ); + } + public getInstalledItems(pageLink: PageLink, itemTypes?: string | string[], config?: IotHubRequestConfig): Observable> { let query = pageLink.toQuery(); if (itemTypes) { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts index 59246aae18..c43d284595 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts @@ -38,12 +38,12 @@ export class IotHubActionsService { private iotHubApiService: IotHubApiService ) {} - openItemDetail(item: MpItemVersionView, installedItem?: IotHubInstalledItem, + openItemDetail(item: MpItemVersionView, installedItem?: IotHubInstalledItem, installedItemsCount?: number, mode?: IotHubItemDetailDialogMode, showCreator?: boolean): Observable { return this.dialog.open(TbIotHubItemDetailDialogComponent, { panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], autoFocus: false, - data: { item, installedItem, mode, showCreator } as IotHubItemDetailDialogData + data: { item, installedItem, installedItemsCount, mode, showCreator } as IotHubItemDetailDialogData }).afterClosed(); } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html index 523b1e11e1..36aafc21e1 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html @@ -461,6 +461,7 @@ = {}; private searchSubject = new Subject(); private destroy$ = new Subject(); @@ -165,8 +165,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadInstalledWidgets(); } else if (this.activeType === ItemType.SOLUTION_TEMPLATE) { this.loadInstalledSolutionTemplates(); - } else if (this.activeType === ItemType.DEVICE) { - this.loadInstalledDevices(); + } else { + this.loadInstalledItemCounts(); } this.loadItems(); } @@ -206,8 +206,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadInstalledWidgets(); } else if (type === ItemType.SOLUTION_TEMPLATE) { this.loadInstalledSolutionTemplates(); - } else if (type === ItemType.DEVICE) { - this.loadInstalledDevices(); + } else { + this.loadInstalledItemCounts(); } this.loadItems(); } @@ -555,14 +555,15 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { if (this.activeType === ItemType.SOLUTION_TEMPLATE && this.installedSolutionTemplates) { return this.installedSolutionTemplates.find(i => i.itemId === item.itemId); } - if (this.activeType === ItemType.DEVICE && this.installedDevices) { - return this.installedDevices.find(i => i.itemId === item.itemId); - } return undefined; } + getInstalledItemsCount(item: MpItemVersionView): number { + return this.installedItemCounts[item.itemId] || 0; + } + openItemDetail(item: MpItemVersionView): void { - this.iotHubActions.openItemDetail(item, this.getInstalledItem(item), this.mode).subscribe(result => { + this.iotHubActions.openItemDetail(item, this.getInstalledItem(item), this.getInstalledItemsCount(item), this.mode).subscribe(result => { if (result?.action === 'add') { this.addItem.emit(result.item); } else if (result === 'installed' || result === 'updated' || result === 'deleted') { @@ -644,11 +645,10 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { }); } - private loadInstalledDevices(): void { - const pageLink = new PageLink(10000, 0); - this.iotHubApiService.getInstalledItems(pageLink, 'DEVICE', {ignoreLoading: true, ignoreErrors: true}).subscribe({ - next: (data) => { - this.installedDevices = data.data; + private loadInstalledItemCounts(): void { + this.iotHubApiService.getInstalledItemCounts(this.activeType, {ignoreLoading: true}).subscribe({ + next: (counts) => { + this.installedItemCounts = counts; } }); } @@ -676,9 +676,9 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.iotHubApiService.getInstalledItems(pageLink, ItemType.SOLUTION_TEMPLATE, config).subscribe(data => { this.installedSolutionTemplates = data.data; }); - } else if (this.activeType === ItemType.DEVICE) { - this.iotHubApiService.getInstalledItems(pageLink, 'DEVICE', config).subscribe(data => { - this.installedDevices = data.data; + } else { + this.iotHubApiService.getInstalledItemCounts(this.activeType, config).subscribe(counts => { + this.installedItemCounts = counts; }); } } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts index ca9fd11af6..8eeddc231b 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts @@ -35,6 +35,7 @@ export class TbIotHubItemCardComponent { @Input() item: MpItemVersionView; @Input() installedItem: IotHubInstalledItem; + @Input() installedItemsCount = 0; @Input() showCreator = true; @Input() showTypeChip = true; @Input() showSubtype = false; diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html index 9a84bed940..46f07934ec 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html @@ -26,26 +26,50 @@
{{ item.name }}
- {{ getTypeIcon() }} - {{ typeTranslations.get(item.type) | translate }} + + {{ getTypeIcon() }} + {{ typeTranslations.get(item.type) | translate }} + - download - {{ item.totalInstallCount | shortNumber }} {{ 'iot-hub.installs' | translate }} + + download + {{ item.totalInstallCount | shortNumber }} {{ 'iot-hub.installs' | translate }} + @if (getSubtypeLabel(); as subtypeLabel) { - {{ subtypeLabel }} + {{ subtypeLabel }} } - v {{ item.version }} + + update + v {{ item.version }} + @if (item.publishedTime) { - {{ item.publishedTime | date:'mediumDate' }} + + today + {{ item.publishedTime | date:'mediumDate' }} + }
- +
+ @if (isInstalled()) { + @if (hasUpdate()) { + + } @else { + + check + {{ 'iot-hub.installed' | translate }} v{{ installedItem.version }} + + } + } + +
@@ -56,57 +80,18 @@
- @if (isInstalled()) { -
- - check - {{ 'iot-hub.installed' | translate }} v{{ installedItem.version }} - - - open_in_new - {{ 'iot-hub.open-item-type' | translate:{ type: getTypeLabel() } }} - -
- } @if (item.description) {
} - @if (showCreator) { -
- person - {{ item.creatorDisplayName }} -
- }
- @if (mode === 'add') { - @if (!isInstalled()) { - - } - } @else { - @if (!isInstalled()) { - - } @else if (hasUpdate()) { - - } - @if (isInstalled()) { - @if (item.type !== ItemType.WIDGET && item.type !== ItemType.SOLUTION_TEMPLATE) { - - } - } } @@ -167,80 +152,19 @@
- - @if (isInstalled()) { - @if (item.type === ItemType.SOLUTION_TEMPLATE) { - - - check - {{ 'iot-hub.installed' | translate }} v{{ installedItem.version }} - - - } @else { - -
- - check - {{ 'iot-hub.installed' | translate }} v{{ installedItem.version }} - - - open_in_new - {{ 'iot-hub.open-item-type' | translate:{ type: getTypeLabel() } }} - -
- } - } @if (item.description) {
} - @if (showCreator) { -
- person - {{ item.creatorDisplayName }} -
- }
- @if (mode === 'add') { - @if (!isInstalled()) { - - } - } @else { - @if (!isInstalled()) { - - } @else if (hasUpdate()) { - - } - @if (isInstalled()) { - @if (item.type !== ItemType.WIDGET && item.type !== ItemType.SOLUTION_TEMPLATE) { - - } - } } @@ -254,6 +178,24 @@
+ @if (showCreator) { +
+ {{ 'iot-hub.creator' | translate }} +
+
+ @if (getCreatorAvatarUrl(); as avatarUrl) { + + } @else { + person + } + @if (item.creatorVerified) { + verified + } +
+ {{ item.creatorDisplayName }} +
+
+ } @if (item.type === ItemType.DEVICE) { @if (item.dataDescriptor?.hardwareType) {
@@ -295,6 +237,39 @@
} + @if ((isInstalled() || installedItemsCount > 0) && mode !== 'add') { +
+ {{ 'iot-hub.manage' | translate }} +
+ @if (item.type === ItemType.SOLUTION_TEMPLATE) { + + + } @else { + @if (installedItemsCount > 0) { + + } @else { + + } + } +
+
+ }
@@ -327,5 +302,30 @@
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss index d326f6e7a6..5c73db517b 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss @@ -51,6 +51,7 @@ gap: 16px; padding: 16px 12px 16px 24px; flex-shrink: 0; + } // Header icon for CF/RC — Design: 64px, rounded-8, colored bg, white icon 36px @@ -87,11 +88,11 @@ color: rgba(0, 0, 0, 0.87); } -// Subtitle row — Design: 13px Regular, rgba(0,0,0,0.54), gap-12 between groups, gap-4 within +// Subtitle row — Design: 13px Regular, rgba(0,0,0,0.54), gap-12 between groups .dlg-subtitle { display: flex; align-items: center; - gap: 4px; + gap: 12px; font-size: 13px; line-height: 16px; letter-spacing: 0.4px; @@ -99,20 +100,33 @@ flex-wrap: wrap; } +.dlg-subtitle-group { + display: inline-flex; + align-items: center; + gap: 4px; +} + .dlg-subtitle-icon { font-size: 16px; width: 16px; height: 16px; + color: rgba(0, 0, 0, 0.54); } -// Dot separator — Design: 4px circle, rgba(0,0,0,0.54) +// Dot separator — Design: 3px circle, rgba(0,0,0,0.38) .dlg-dot { - width: 4px; - height: 4px; - border-radius: 2px; - background: rgba(0, 0, 0, 0.54); + width: 3px; + height: 3px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.38); + flex-shrink: 0; +} + +.dlg-header-actions { + display: flex; + align-items: center; + gap: 8px; flex-shrink: 0; - margin: 0 4px; } .dlg-close { @@ -274,13 +288,64 @@ .dlg-meta { display: flex; gap: 24px; - padding: 16px 24px; + padding: 16px 24px 12px; } .dlg-meta-group { display: flex; flex-direction: column; + gap: 4px; +} + +.dlg-meta-creator { + display: flex; + align-items: center; gap: 8px; + padding: 4px 0; + + &.clickable { + cursor: pointer; + } +} + +.dlg-meta-creator-avatar { + position: relative; + width: 32px; + height: 32px; + flex-shrink: 0; + + img { + width: 32px; + height: 32px; + border-radius: 9999px; + object-fit: cover; + } + + mat-icon:not(.dlg-meta-creator-verified) { + font-size: 32px; + width: 32px; + height: 32px; + color: rgba(0, 0, 0, 0.38); + } + + .dlg-meta-creator-verified { + position: absolute; + top: -6px; + left: 18px; + font-size: 20px; + width: 20px; + height: 20px; + color: #00695c; + } +} + +.dlg-meta-creator-name { + font-size: 12px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.76); + white-space: nowrap; } // Meta label — Design: 12px Medium, rgba(0,0,0,0.54), tracking 0.25 @@ -295,7 +360,30 @@ .dlg-meta-chips { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 4px; + padding: 8px 0; +} + +.dlg-meta-manage { + margin-left: auto; + flex-shrink: 0; +} + +.dlg-meta-manage-actions { + display: flex; + align-items: center; +} + +.dlg-manage-icon-btn.mat-mdc-icon-button { + width: 40px; + height: 40px; + padding: 8px; + --mdc-icon-button-state-layer-size: 40px; + color: rgba(0, 0, 0, 0.54); +} + +.dlg-manage-remove-btn { + color: rgba(0, 0, 0, 0.54); } // Chip — Design: 12px Medium, h-24, p-4 px-8, rounded-4 @@ -550,7 +638,7 @@ .dlg-installed-badge { display: inline-flex; align-items: center; - align-self: flex-start; + flex-shrink: 0; gap: 4px; padding: 4px 8px; border-radius: 16px; @@ -568,6 +656,29 @@ } } +// Update button — Design: border #ff5722, text #ff5722, rounded-4, px-16 py-6 +.dlg-update-btn { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 6px 16px; + border: 1px solid #ff5722; + border-radius: 4px; + background: transparent; + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.25px; + color: #ff5722; + white-space: nowrap; + cursor: pointer; + + &:hover { + background: rgba(255, 87, 34, 0.04); + } +} + // Open details / Solution instructions link — Design: 12px Medium, $tb-primary-color .dlg-info-link { display: inline-flex; @@ -615,10 +726,11 @@ } } -// Footer — only Close button +// Footer .dlg-footer { display: flex; align-items: center; + gap: 8px; padding: 8px; border-top: 1px solid rgba(0, 0, 0, 0.12); flex-shrink: 0; diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts index 05d72cc093..7518b1b803 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts @@ -39,6 +39,7 @@ export type IotHubItemDetailDialogMode = 'default' | 'add'; export interface IotHubItemDetailDialogData { item: MpItemVersionView; installedItem?: IotHubInstalledItem; + installedItemsCount?: number; mode?: IotHubItemDetailDialogMode; showCreator?: boolean; } @@ -58,6 +59,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent { @@ -373,6 +373,10 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent = {}; + installedDashboardCounts: Record = {}; + installedCalcFieldCounts: Record = {}; + installedRuleChainCounts: Record = {}; private searchSubject = new Subject(); private searchSubscription: Subscription; @@ -205,9 +209,24 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { } } + getInstalledItemsCount(item: MpItemVersionView): number { + switch (item.type) { + case ItemType.DEVICE: + return this.installedDeviceCounts[item.itemId] || 0; + case ItemType.DASHBOARD: + return this.installedDashboardCounts[item.itemId] || 0; + case ItemType.CALCULATED_FIELD: + return this.installedCalcFieldCounts[item.itemId] || 0; + case ItemType.RULE_CHAIN: + return this.installedRuleChainCounts[item.itemId] || 0; + default: + return 0; + } + } + // Dialogs openItemDetail(item: MpItemVersionView): void { - this.iotHubActions.openItemDetail(item, this.getInstalledItem(item), undefined, this.showCreator).subscribe(result => { + this.iotHubActions.openItemDetail(item, this.getInstalledItem(item), this.getInstalledItemsCount(item), undefined, this.showCreator).subscribe(result => { if (result === 'installed' || result === 'deleted' || result === 'updated') { this.reloadInstalledItems(); } @@ -286,10 +305,18 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { const pageLink = new PageLink(10000, 0); forkJoin({ widgets: this.iotHubApiService.getInstalledItems(pageLink, ItemType.WIDGET, config), - solutionTemplates: this.iotHubApiService.getInstalledItems(pageLink, ItemType.SOLUTION_TEMPLATE, config) + solutionTemplates: this.iotHubApiService.getInstalledItems(pageLink, ItemType.SOLUTION_TEMPLATE, config), + deviceCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.DEVICE, config), + dashboardCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.DASHBOARD, config), + calcFieldCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.CALCULATED_FIELD, config), + ruleChainCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.RULE_CHAIN, config) }).subscribe(results => { this.installedWidgets = results.widgets.data; this.installedSolutionTemplates = results.solutionTemplates.data; + this.installedDeviceCounts = results.deviceCounts; + this.installedDashboardCounts = results.dashboardCounts; + this.installedCalcFieldCounts = results.calcFieldCounts; + this.installedRuleChainCounts = results.ruleChainCounts; }); } 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 790c437302..00984163c4 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 @@ -275,14 +275,12 @@ @for (item of popularDevices; track item.id) { + (installClick)="installItem($event)"> }
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 61c6c33453..d978f99f56 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 @@ -128,7 +128,10 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { installedWidgets: IotHubInstalledItem[] = []; installedSolutionTemplates: IotHubInstalledItem[] = []; - installedDevices: IotHubInstalledItem[] = []; + installedDeviceCounts: Record = {}; + installedDashboardCounts: Record = {}; + installedCalcFieldCounts: Record = {}; + installedRuleChainCounts: Record = {}; installedItemsCount = 0; isLoading = true; @@ -310,7 +313,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { } openItemDetail(item: MpItemVersionView): void { - this.iotHubActions.openItemDetail(item, this.findInstalledItem(item)).subscribe(result => { + this.iotHubActions.openItemDetail(item, this.findInstalledItem(item), this.findInstalledItemsCount(item)).subscribe(result => { if (result === 'installed' || result === 'deleted') { this.reloadInstalledItems(item.type); } @@ -332,8 +335,20 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { this.installedSolutionTemplates = data.data; }); } else if (type === ItemType.DEVICE) { - this.iotHubApiService.getInstalledItems(pageLink, ItemType.DEVICE, config).subscribe(data => { - this.installedDevices = data.data; + this.iotHubApiService.getInstalledItemCounts(ItemType.DEVICE, config).subscribe(counts => { + this.installedDeviceCounts = counts; + }); + } else if (type === ItemType.DASHBOARD) { + this.iotHubApiService.getInstalledItemCounts(ItemType.DASHBOARD, config).subscribe(counts => { + this.installedDashboardCounts = counts; + }); + } else if (type === ItemType.CALCULATED_FIELD) { + this.iotHubApiService.getInstalledItemCounts(ItemType.CALCULATED_FIELD, config).subscribe(counts => { + this.installedCalcFieldCounts = counts; + }); + } else if (type === ItemType.RULE_CHAIN) { + this.iotHubApiService.getInstalledItemCounts(ItemType.RULE_CHAIN, config).subscribe(counts => { + this.installedRuleChainCounts = counts; }); } } @@ -344,13 +359,26 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { return this.installedWidgets.find(i => i.itemId === item.itemId); case ItemType.SOLUTION_TEMPLATE: return this.installedSolutionTemplates.find(i => i.itemId === item.itemId); - case ItemType.DEVICE: - return this.installedDevices.find(i => i.itemId === item.itemId); default: return undefined; } } + findInstalledItemsCount(item: MpItemVersionView): number { + switch (item.type) { + case ItemType.DEVICE: + return this.installedDeviceCounts[item.itemId] || 0; + case ItemType.DASHBOARD: + return this.installedDashboardCounts[item.itemId] || 0; + case ItemType.CALCULATED_FIELD: + return this.installedCalcFieldCounts[item.itemId] || 0; + case ItemType.RULE_CHAIN: + return this.installedRuleChainCounts[item.itemId] || 0; + default: + return 0; + } + } + installItem(item: MpItemVersionView): void { this.iotHubActions.installItem(item).subscribe(result => { if (result === 'installed') { @@ -381,10 +409,6 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { return this.installedSolutionTemplates.find(i => i.itemId === item.itemId); } - getInstalledDevice(item: MpItemVersionView): IotHubInstalledItem | undefined { - return this.installedDevices.find(i => i.itemId === item.itemId); - } - deleteInstalledItem(item: MpItemVersionView): void { const installedItem = this.findInstalledItem(item); if (!installedItem) { return; } @@ -394,8 +418,14 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { this.installedWidgets = this.installedWidgets.filter(i => i.id.id !== installedItem.id.id); } else if (item.type === ItemType.SOLUTION_TEMPLATE) { this.installedSolutionTemplates = this.installedSolutionTemplates.filter(i => i.id.id !== installedItem.id.id); - } else if (item.type === ItemType.DEVICE) { - this.installedDevices = this.installedDevices.filter(i => i.id.id !== installedItem.id.id); + } else if (item.type === ItemType.DEVICE && this.installedDeviceCounts[item.itemId]) { + this.installedDeviceCounts[item.itemId] = Math.max(0, this.installedDeviceCounts[item.itemId] - 1); + } else if (item.type === ItemType.DASHBOARD && this.installedDashboardCounts[item.itemId]) { + this.installedDashboardCounts[item.itemId] = Math.max(0, this.installedDashboardCounts[item.itemId] - 1); + } else if (item.type === ItemType.CALCULATED_FIELD && this.installedCalcFieldCounts[item.itemId]) { + this.installedCalcFieldCounts[item.itemId] = Math.max(0, this.installedCalcFieldCounts[item.itemId] - 1); + } else if (item.type === ItemType.RULE_CHAIN && this.installedRuleChainCounts[item.itemId]) { + this.installedRuleChainCounts[item.itemId] = Math.max(0, this.installedRuleChainCounts[item.itemId] - 1); } }); } @@ -455,7 +485,10 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { devices: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DEVICE, this.bigCardCount), config), installedWidgets: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.WIDGET, config), installedSolutionTemplates: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.SOLUTION_TEMPLATE, config), - installedDevices: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.DEVICE, config), + installedDeviceCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.DEVICE, config), + installedDashboardCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.DASHBOARD, config), + installedCalcFieldCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.CALCULATED_FIELD, config), + installedRuleChainCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.RULE_CHAIN, config), installedCount: this.iotHubApiService.getInstalledItemsCount(null, config) }).subscribe({ next: (results) => { @@ -467,7 +500,10 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { this.popularDevices = results.devices.data; this.installedWidgets = results.installedWidgets.data; this.installedSolutionTemplates = results.installedSolutionTemplates.data; - this.installedDevices = results.installedDevices.data; + this.installedDeviceCounts = results.installedDeviceCounts; + this.installedDashboardCounts = results.installedDashboardCounts; + this.installedCalcFieldCounts = results.installedCalcFieldCounts; + this.installedRuleChainCounts = results.installedRuleChainCounts; this.installedItemsCount = results.installedCount; this.isLoading = false; }, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 0d91179da1..a613c235f0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3772,6 +3772,8 @@ "retry": "Retry", "no-items-found": "No items found", "no-items-found-text": "Try adjusting your search or filters.", + "creator": "Creator", + "manage": "Manage", "become-a-creator": "Become a creator", "back-to-iot-hub": "Back to IoT Hub", "verified-creator": "Verified Creator", @@ -3799,6 +3801,7 @@ "install-type": "Type: {{type}}", "install-creator": "Creator: {{creator}}", "install": "Install", + "connect-device": "Connect device", "install-one-more": "Install one more", "installing": "Installing...", "installed": "Installed", @@ -3815,6 +3818,7 @@ "updates": "Updates", "up-to-date": "Up to date", "update": "Update", + "update-to-version": "Update to v {{ version }}", "update-item-title": "Update Item", "update-confirm": "Update '{{name}}' to version {{version}}?", "update-confirm-title": "Update '{{name}}'?",