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}}'?",