Browse Source

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.
pull/15508/head
Igor Kulikov 4 months ago
parent
commit
8e8efed94c
  1. 8
      application/src/main/java/org/thingsboard/server/controller/IotHubController.java
  2. 3
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java
  3. 3
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java
  4. 6
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java
  5. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java
  6. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java
  7. 7
      ui-ngx/src/app/core/http/iot-hub-api.service.ts
  8. 4
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts
  9. 1
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html
  10. 40
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts
  11. 1
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts
  12. 240
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html
  13. 136
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss
  14. 22
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
  15. 1
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.html
  16. 31
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts
  17. 6
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html
  18. 64
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
  19. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

8
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 org.thingsboard.server.common.data.page.PageLink;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.dao.device.DeviceConnectivityService; import org.thingsboard.server.dao.device.DeviceConnectivityService;
@ -117,6 +118,13 @@ public class IotHubController extends BaseController {
return iotHubInstalledItemService.findInstalledItemIdsByTenantId(getTenantId()); return iotHubInstalledItemService.findInstalledItemIdsByTenantId(getTenantId());
} }
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/installedItems/counts")
@ResponseBody
public Map<UUID, Long> getInstalledItemCounts(@RequestParam String itemType) throws ThingsboardException {
return iotHubInstalledItemService.findInstalledItemCounts(getTenantId(), itemType);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@DeleteMapping("/installedItems/{installedItemId}") @DeleteMapping("/installedItems/{installedItemId}")
@ResponseBody @ResponseBody

3
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 org.thingsboard.server.dao.Dao;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public interface IotHubInstalledItemDao extends Dao<IotHubInstalledItem> { public interface IotHubInstalledItemDao extends Dao<IotHubInstalledItem> {
@ -32,6 +33,8 @@ public interface IotHubInstalledItemDao extends Dao<IotHubInstalledItem> {
long countByTenantId(TenantId tenantId, String itemType); long countByTenantId(TenantId tenantId, String itemType);
Map<UUID, Long> findInstalledItemCounts(TenantId tenantId, String itemType);
void deleteByTenantId(TenantId tenantId); void deleteByTenantId(TenantId tenantId);
} }

3
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 org.thingsboard.server.common.data.page.PageLink;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public interface IotHubInstalledItemService { public interface IotHubInstalledItemService {
@ -36,6 +37,8 @@ public interface IotHubInstalledItemService {
long countByTenantId(TenantId tenantId, String itemType); long countByTenantId(TenantId tenantId, String itemType);
Map<UUID, Long> findInstalledItemCounts(TenantId tenantId, String itemType);
void deleteById(TenantId tenantId, IotHubInstalledItemId id); void deleteById(TenantId tenantId, IotHubInstalledItemId id);
void deleteByTenantId(TenantId tenantId); void deleteByTenantId(TenantId tenantId);

6
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 org.thingsboard.server.common.data.page.PageLink;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
@Service @Service
@ -60,6 +61,11 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService {
return iotHubInstalledItemDao.countByTenantId(tenantId, itemType); return iotHubInstalledItemDao.countByTenantId(tenantId, itemType);
} }
@Override
public Map<UUID, Long> findInstalledItemCounts(TenantId tenantId, String itemType) {
return iotHubInstalledItemDao.findInstalledItemCounts(tenantId, itemType);
}
@Override @Override
public void deleteById(TenantId tenantId, IotHubInstalledItemId id) { public void deleteById(TenantId tenantId, IotHubInstalledItemId id) {
iotHubInstalledItemDao.removeById(tenantId, id.getId()); iotHubInstalledItemDao.removeById(tenantId, id.getId());

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

@ -50,6 +50,14 @@ interface IotHubInstalledItemRepository extends JpaRepository<IotHubInstalledIte
""") """)
long countByTenantId(@Param("tenantId") UUID tenantId, @Param("itemType") String itemType); long countByTenantId(@Param("tenantId") UUID tenantId, @Param("itemType") String itemType);
@Query("""
SELECT item.itemId, COUNT(item) FROM IotHubInstalledItemEntity item
WHERE item.tenantId = :tenantId
AND item.itemType = :itemType
GROUP BY item.itemId
""")
List<Object[]> findInstalledItemCounts(@Param("tenantId") UUID tenantId, @Param("itemType") String itemType);
@Transactional @Transactional
@Modifying @Modifying
@Query(value = "DELETE FROM iot_hub_installed_item WHERE tenant_id = :tenantId", nativeQuery = true) @Query(value = "DELETE FROM iot_hub_installed_item WHERE tenant_id = :tenantId", nativeQuery = true)

12
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.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
@SqlDao @SqlDao
@ -62,6 +64,16 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao<IotHubInstalledItemEntity
return repository.countByTenantId(tenantId.getId(), itemType); return repository.countByTenantId(tenantId.getId(), itemType);
} }
@Override
public Map<UUID, Long> findInstalledItemCounts(TenantId tenantId, String itemType) {
List<Object[]> results = repository.findInstalledItemCounts(tenantId.getId(), itemType);
Map<UUID, Long> counts = new HashMap<>();
for (Object[] row : results) {
counts.put((UUID) row[0], (Long) row[1]);
}
return counts;
}
@Override @Override
public void deleteByTenantId(TenantId tenantId) { public void deleteByTenantId(TenantId tenantId) {
repository.deleteByTenantId(tenantId.getId()); repository.deleteByTenantId(tenantId.getId());

7
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<Record<string, number>> {
return this.http.get<Record<string, number>>(
`/api/iot-hub/installedItems/counts?itemType=${itemType}`,
{ params: this.buildParams(config) }
);
}
public getInstalledItems(pageLink: PageLink, itemTypes?: string | string[], config?: IotHubRequestConfig): Observable<PageData<IotHubInstalledItem>> { public getInstalledItems(pageLink: PageLink, itemTypes?: string | string[], config?: IotHubRequestConfig): Observable<PageData<IotHubInstalledItem>> {
let query = pageLink.toQuery(); let query = pageLink.toQuery();
if (itemTypes) { if (itemTypes) {

4
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts

@ -38,12 +38,12 @@ export class IotHubActionsService {
private iotHubApiService: IotHubApiService private iotHubApiService: IotHubApiService
) {} ) {}
openItemDetail(item: MpItemVersionView, installedItem?: IotHubInstalledItem, openItemDetail(item: MpItemVersionView, installedItem?: IotHubInstalledItem, installedItemsCount?: number,
mode?: IotHubItemDetailDialogMode, showCreator?: boolean): Observable<any> { mode?: IotHubItemDetailDialogMode, showCreator?: boolean): Observable<any> {
return this.dialog.open(TbIotHubItemDetailDialogComponent, { return this.dialog.open(TbIotHubItemDetailDialogComponent, {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
autoFocus: false, autoFocus: false,
data: { item, installedItem, mode, showCreator } as IotHubItemDetailDialogData data: { item, installedItem, installedItemsCount, mode, showCreator } as IotHubItemDetailDialogData
}).afterClosed(); }).afterClosed();
} }

1
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html

@ -461,6 +461,7 @@
<tb-iot-hub-item-card <tb-iot-hub-item-card
[item]="item" [item]="item"
[installedItem]="getInstalledItem(item)" [installedItem]="getInstalledItem(item)"
[installedItemsCount]="getInstalledItemsCount(item)"
[showTypeChip]="false" [showTypeChip]="false"
[showSubtype]="!fixedSubType" [showSubtype]="!fixedSubType"
[showCreator]="!creatorId" [showCreator]="!creatorId"

40
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts

@ -71,12 +71,12 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
// trigger type-specific loading since our ngOnInit already ran // trigger type-specific loading since our ngOnInit already ran
if (wasInit) { if (wasInit) {
this.loadFilterInfo(); this.loadFilterInfo();
if (value === ItemType.DEVICE) { if (value === ItemType.WIDGET) {
this.loadInstalledDevices();
} else if (value === ItemType.WIDGET) {
this.loadInstalledWidgets(); this.loadInstalledWidgets();
} else if (value === ItemType.SOLUTION_TEMPLATE) { } else if (value === ItemType.SOLUTION_TEMPLATE) {
this.loadInstalledSolutionTemplates(); this.loadInstalledSolutionTemplates();
} else {
this.loadInstalledItemCounts();
} }
} }
} }
@ -128,7 +128,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
installedWidgets: IotHubInstalledItem[] = null; installedWidgets: IotHubInstalledItem[] = null;
installedSolutionTemplates: IotHubInstalledItem[] = null; installedSolutionTemplates: IotHubInstalledItem[] = null;
installedDevices: IotHubInstalledItem[] = null; installedItemCounts: Record<string, number> = {};
private searchSubject = new Subject<string>(); private searchSubject = new Subject<string>();
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -165,8 +165,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
this.loadInstalledWidgets(); this.loadInstalledWidgets();
} else if (this.activeType === ItemType.SOLUTION_TEMPLATE) { } else if (this.activeType === ItemType.SOLUTION_TEMPLATE) {
this.loadInstalledSolutionTemplates(); this.loadInstalledSolutionTemplates();
} else if (this.activeType === ItemType.DEVICE) { } else {
this.loadInstalledDevices(); this.loadInstalledItemCounts();
} }
this.loadItems(); this.loadItems();
} }
@ -206,8 +206,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
this.loadInstalledWidgets(); this.loadInstalledWidgets();
} else if (type === ItemType.SOLUTION_TEMPLATE) { } else if (type === ItemType.SOLUTION_TEMPLATE) {
this.loadInstalledSolutionTemplates(); this.loadInstalledSolutionTemplates();
} else if (type === ItemType.DEVICE) { } else {
this.loadInstalledDevices(); this.loadInstalledItemCounts();
} }
this.loadItems(); this.loadItems();
} }
@ -555,14 +555,15 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
if (this.activeType === ItemType.SOLUTION_TEMPLATE && this.installedSolutionTemplates) { if (this.activeType === ItemType.SOLUTION_TEMPLATE && this.installedSolutionTemplates) {
return this.installedSolutionTemplates.find(i => i.itemId === item.itemId); 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; return undefined;
} }
getInstalledItemsCount(item: MpItemVersionView): number {
return this.installedItemCounts[item.itemId] || 0;
}
openItemDetail(item: MpItemVersionView): void { 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') { if (result?.action === 'add') {
this.addItem.emit(result.item); this.addItem.emit(result.item);
} else if (result === 'installed' || result === 'updated' || result === 'deleted') { } else if (result === 'installed' || result === 'updated' || result === 'deleted') {
@ -644,11 +645,10 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
}); });
} }
private loadInstalledDevices(): void { private loadInstalledItemCounts(): void {
const pageLink = new PageLink(10000, 0); this.iotHubApiService.getInstalledItemCounts(this.activeType, {ignoreLoading: true}).subscribe({
this.iotHubApiService.getInstalledItems(pageLink, 'DEVICE', {ignoreLoading: true, ignoreErrors: true}).subscribe({ next: (counts) => {
next: (data) => { this.installedItemCounts = counts;
this.installedDevices = data.data;
} }
}); });
} }
@ -676,9 +676,9 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
this.iotHubApiService.getInstalledItems(pageLink, ItemType.SOLUTION_TEMPLATE, config).subscribe(data => { this.iotHubApiService.getInstalledItems(pageLink, ItemType.SOLUTION_TEMPLATE, config).subscribe(data => {
this.installedSolutionTemplates = data.data; this.installedSolutionTemplates = data.data;
}); });
} else if (this.activeType === ItemType.DEVICE) { } else {
this.iotHubApiService.getInstalledItems(pageLink, 'DEVICE', config).subscribe(data => { this.iotHubApiService.getInstalledItemCounts(this.activeType, config).subscribe(counts => {
this.installedDevices = data.data; this.installedItemCounts = counts;
}); });
} }
} }

1
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() item: MpItemVersionView;
@Input() installedItem: IotHubInstalledItem; @Input() installedItem: IotHubInstalledItem;
@Input() installedItemsCount = 0;
@Input() showCreator = true; @Input() showCreator = true;
@Input() showTypeChip = true; @Input() showTypeChip = true;
@Input() showSubtype = false; @Input() showSubtype = false;

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

@ -26,26 +26,50 @@
<div class="dlg-header-content"> <div class="dlg-header-content">
<span class="dlg-title">{{ item.name }}</span> <span class="dlg-title">{{ item.name }}</span>
<div class="dlg-subtitle"> <div class="dlg-subtitle">
<tb-icon class="dlg-subtitle-icon">{{ getTypeIcon() }}</tb-icon> <span class="dlg-subtitle-group">
<span>{{ typeTranslations.get(item.type) | translate }}</span> <tb-icon class="dlg-subtitle-icon">{{ getTypeIcon() }}</tb-icon>
<span>{{ typeTranslations.get(item.type) | translate }}</span>
</span>
<span class="dlg-dot"></span> <span class="dlg-dot"></span>
<tb-icon class="dlg-subtitle-icon">download</tb-icon> <span class="dlg-subtitle-group">
<span>{{ item.totalInstallCount | shortNumber }} {{ 'iot-hub.installs' | translate }}</span> <tb-icon class="dlg-subtitle-icon">download</tb-icon>
<span>{{ item.totalInstallCount | shortNumber }} {{ 'iot-hub.installs' | translate }}</span>
</span>
@if (getSubtypeLabel(); as subtypeLabel) { @if (getSubtypeLabel(); as subtypeLabel) {
<span class="dlg-dot"></span> <span class="dlg-dot"></span>
<span>{{ subtypeLabel }}</span> <span class="dlg-subtitle-group">{{ subtypeLabel }}</span>
} }
<span class="dlg-dot"></span> <span class="dlg-dot"></span>
<span>v {{ item.version }}</span> <span class="dlg-subtitle-group">
<mat-icon class="dlg-subtitle-icon">update</mat-icon>
<span>v {{ item.version }}</span>
</span>
@if (item.publishedTime) { @if (item.publishedTime) {
<span class="dlg-dot"></span> <span class="dlg-dot"></span>
<span>{{ item.publishedTime | date:'mediumDate' }}</span> <span class="dlg-subtitle-group">
<mat-icon class="dlg-subtitle-icon">today</mat-icon>
<span>{{ item.publishedTime | date:'mediumDate' }}</span>
</span>
} }
</div> </div>
</div> </div>
<button mat-icon-button class="dlg-close" (click)="close()" type="button"> <div class="dlg-header-actions">
<tb-icon>close</tb-icon> @if (isInstalled()) {
</button> @if (hasUpdate()) {
<button class="dlg-update-btn" (click)="updateItem()">
{{ 'iot-hub.update-to-version' | translate:{ version: item.version } }}
</button>
} @else {
<span class="dlg-installed-badge">
<mat-icon>check</mat-icon>
{{ 'iot-hub.installed' | translate }} v{{ installedItem.version }}
</span>
}
}
<button mat-icon-button class="dlg-close" (click)="close()" type="button">
<tb-icon>close</tb-icon>
</button>
</div>
</div> </div>
<!-- Scrollable content --> <!-- Scrollable content -->
@ -56,57 +80,18 @@
<!-- CF/RC/Device: description + author (no image) --> <!-- CF/RC/Device: description + author (no image) -->
<div class="dlg-info dlg-info-compact"> <div class="dlg-info dlg-info-compact">
<div class="dlg-info-text"> <div class="dlg-info-text">
@if (isInstalled()) {
<div class="dlg-info-status-row">
<span class="dlg-installed-badge">
<mat-icon>check</mat-icon>
{{ 'iot-hub.installed' | translate }} v{{ installedItem.version }}
</span>
<a class="dlg-info-link" (click)="openEntityDetails()">
<mat-icon>open_in_new</mat-icon>
{{ 'iot-hub.open-item-type' | translate:{ type: getTypeLabel() } }}
</a>
</div>
}
@if (item.description) { @if (item.description) {
<div class="dlg-description"> <div class="dlg-description">
<tb-markdown [data]="item.description"></tb-markdown> <tb-markdown [data]="item.description"></tb-markdown>
</div> </div>
} }
@if (showCreator) {
<div class="dlg-author" [class.clickable]="mode !== 'add'" (click)="mode !== 'add' && navigateToCreator()">
<tb-icon>person</tb-icon>
<span>{{ item.creatorDisplayName }}</span>
</div>
}
</div> </div>
<div class="dlg-info-actions"> <div class="dlg-info-actions">
@if (mode === 'add') { @if (mode !== 'add' && isInstalled()) {
@if (!isInstalled()) { @if (item.type !== ItemType.WIDGET && item.type !== ItemType.SOLUTION_TEMPLATE) {
<button mat-flat-button color="primary" (click)="addItem()"> <button mat-stroked-button (click)="install()">
{{ 'action.add' | translate }} <mat-icon>add</mat-icon>
</button> {{ 'iot-hub.install-one-more' | translate }}
}
} @else {
@if (!isInstalled()) {
<button mat-flat-button color="primary" (click)="install()">
{{ 'iot-hub.install' | translate }}
</button>
} @else if (hasUpdate()) {
<button mat-flat-button color="primary" (click)="updateItem()">
{{ 'iot-hub.update' | translate }}
</button>
}
@if (isInstalled()) {
@if (item.type !== ItemType.WIDGET && item.type !== ItemType.SOLUTION_TEMPLATE) {
<button mat-stroked-button (click)="install()">
<mat-icon>add</mat-icon>
{{ 'iot-hub.install-one-more' | translate }}
</button>
}
<button mat-stroked-button color="warn" (click)="deleteItem()">
<mat-icon>delete</mat-icon>
{{ 'iot-hub.remove' | translate }}
</button> </button>
} }
} }
@ -167,80 +152,19 @@
</div> </div>
<div class="dlg-info"> <div class="dlg-info">
<div class="dlg-info-text"> <div class="dlg-info-text">
<!-- Installed status + links -->
@if (isInstalled()) {
@if (item.type === ItemType.SOLUTION_TEMPLATE) {
<!-- Solution template: badge on own row, links on separate row -->
<span class="dlg-installed-badge">
<mat-icon>check</mat-icon>
{{ 'iot-hub.installed' | translate }} v{{ installedItem.version }}
</span>
<div class="dlg-info-status-row">
@if (isSameVersion()) {
<a class="dlg-info-link" (click)="openSolutionInstructions()">
<mat-icon>info_outline</mat-icon>
{{ 'iot-hub.solution-instructions' | translate }}
</a>
}
<a class="dlg-info-link" (click)="openEntityDetails()">
<mat-icon>open_in_new</mat-icon>
{{ 'iot-hub.goto-main-dashboard' | translate }}
</a>
</div>
} @else {
<!-- Other types: badge + link on same row -->
<div class="dlg-info-status-row">
<span class="dlg-installed-badge">
<mat-icon>check</mat-icon>
{{ 'iot-hub.installed' | translate }} v{{ installedItem.version }}
</span>
<a class="dlg-info-link" (click)="openEntityDetails()">
<mat-icon>open_in_new</mat-icon>
{{ 'iot-hub.open-item-type' | translate:{ type: getTypeLabel() } }}
</a>
</div>
}
}
@if (item.description) { @if (item.description) {
<div class="dlg-description"> <div class="dlg-description">
<tb-markdown [data]="item.description"></tb-markdown> <tb-markdown [data]="item.description"></tb-markdown>
</div> </div>
} }
@if (showCreator) {
<div class="dlg-author" [class.clickable]="mode !== 'add'" (click)="mode !== 'add' && navigateToCreator()">
<tb-icon>person</tb-icon>
<span>{{ item.creatorDisplayName }}</span>
</div>
}
</div> </div>
<!-- Action buttons --> <!-- Action buttons -->
<div class="dlg-info-actions"> <div class="dlg-info-actions">
@if (mode === 'add') { @if (mode !== 'add' && isInstalled()) {
@if (!isInstalled()) { @if (item.type !== ItemType.WIDGET && item.type !== ItemType.SOLUTION_TEMPLATE) {
<button mat-flat-button color="primary" (click)="addItem()"> <button mat-stroked-button (click)="install()">
{{ 'action.add' | translate }} <mat-icon>add</mat-icon>
</button> {{ 'iot-hub.install-one-more' | translate }}
}
} @else {
@if (!isInstalled()) {
<button mat-flat-button color="primary" (click)="install()">
{{ 'iot-hub.install' | translate }}
</button>
} @else if (hasUpdate()) {
<button mat-flat-button color="primary" (click)="updateItem()">
{{ 'iot-hub.update' | translate }}
</button>
}
@if (isInstalled()) {
@if (item.type !== ItemType.WIDGET && item.type !== ItemType.SOLUTION_TEMPLATE) {
<button mat-stroked-button (click)="install()">
<mat-icon>add</mat-icon>
{{ 'iot-hub.install-one-more' | translate }}
</button>
}
<button mat-stroked-button color="warn" (click)="deleteItem()">
<tb-icon matButtonIcon class="tb-mat-24">mdi:trash-can-outline</tb-icon>
{{ 'iot-hub.remove' | translate }}
</button> </button>
} }
} }
@ -254,6 +178,24 @@
<div class="dlg-meta-bar"> <div class="dlg-meta-bar">
<div class="dlg-divider"></div> <div class="dlg-divider"></div>
<div class="dlg-meta"> <div class="dlg-meta">
@if (showCreator) {
<div class="dlg-meta-group">
<span class="dlg-meta-label">{{ 'iot-hub.creator' | translate }}</span>
<div class="dlg-meta-creator" [class.clickable]="mode !== 'add'" (click)="mode !== 'add' && navigateToCreator()">
<div class="dlg-meta-creator-avatar">
@if (getCreatorAvatarUrl(); as avatarUrl) {
<img [src]="avatarUrl" alt="">
} @else {
<mat-icon>person</mat-icon>
}
@if (item.creatorVerified) {
<mat-icon class="dlg-meta-creator-verified">verified</mat-icon>
}
</div>
<span class="dlg-meta-creator-name">{{ item.creatorDisplayName }}</span>
</div>
</div>
}
@if (item.type === ItemType.DEVICE) { @if (item.type === ItemType.DEVICE) {
@if (item.dataDescriptor?.hardwareType) { @if (item.dataDescriptor?.hardwareType) {
<div class="dlg-meta-group"> <div class="dlg-meta-group">
@ -295,6 +237,39 @@
</div> </div>
</div> </div>
} }
@if ((isInstalled() || installedItemsCount > 0) && mode !== 'add') {
<div class="dlg-meta-group dlg-meta-manage">
<span class="dlg-meta-label">{{ 'iot-hub.manage' | translate }}</span>
<div class="dlg-meta-manage-actions">
@if (item.type === ItemType.SOLUTION_TEMPLATE) {
<button mat-icon-button class="dlg-manage-icon-btn"
[matTooltip]="'iot-hub.solution-instructions' | translate"
matTooltipPosition="above"
(click)="openSolutionInstructions()">
<mat-icon>info_outline</mat-icon>
</button>
<button mat-icon-button class="dlg-manage-icon-btn"
[matTooltip]="'iot-hub.remove' | translate"
matTooltipPosition="above"
(click)="deleteItem()">
<tb-icon class="tb-mat-24">mdi:trash-can-outline</tb-icon>
</button>
} @else {
@if (installedItemsCount > 0) {
<button mat-stroked-button color="primary" (click)="openInstalledItemsDialog()">
<mat-icon>inventory_2</mat-icon>
{{ 'iot-hub.installed-items' | translate }}: {{ installedItemsCount }}
</button>
} @else {
<button mat-stroked-button class="dlg-manage-remove-btn" (click)="deleteItem()">
<tb-icon matButtonIcon class="tb-mat-24">mdi:trash-can-outline</tb-icon>
{{ 'iot-hub.remove' | translate }}
</button>
}
}
</div>
</div>
}
</div> </div>
<div class="dlg-divider"></div> <div class="dlg-divider"></div>
</div> </div>
@ -327,5 +302,30 @@
<div class="dlg-footer"> <div class="dlg-footer">
<span class="dlg-footer-spacer"></span> <span class="dlg-footer-spacer"></span>
<button mat-button (click)="close()">{{ 'action.close' | translate }}</button> <button mat-button (click)="close()">{{ 'action.close' | translate }}</button>
@if (mode === 'add' && !isInstalled()) {
<button mat-flat-button color="primary" (click)="addItem()">
{{ 'action.add' | translate }}
</button>
}
@if (!isInstalled() && mode !== 'add') {
@if (item.type === ItemType.DEVICE) {
<button mat-flat-button color="primary" (click)="installDevice()">
{{ 'iot-hub.connect-device' | translate }}
</button>
} @else {
<button mat-flat-button color="primary" (click)="install()">
{{ 'iot-hub.install' | translate }}
</button>
}
}
@if (isInstalled() && mode !== 'add') {
<button mat-flat-button color="primary" (click)="openEntityDetails()">
@if (item.type === ItemType.SOLUTION_TEMPLATE) {
{{ 'iot-hub.goto-main-dashboard' | translate }}
} @else {
{{ 'iot-hub.open-item-type' | translate:{ type: getTypeLabel() } }}
}
</button>
}
</div> </div>
</div> </div>

136
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss

@ -51,6 +51,7 @@
gap: 16px; gap: 16px;
padding: 16px 12px 16px 24px; padding: 16px 12px 16px 24px;
flex-shrink: 0; flex-shrink: 0;
} }
// Header icon for CF/RC Design: 64px, rounded-8, colored bg, white icon 36px // 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); 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 { .dlg-subtitle {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 12px;
font-size: 13px; font-size: 13px;
line-height: 16px; line-height: 16px;
letter-spacing: 0.4px; letter-spacing: 0.4px;
@ -99,20 +100,33 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.dlg-subtitle-group {
display: inline-flex;
align-items: center;
gap: 4px;
}
.dlg-subtitle-icon { .dlg-subtitle-icon {
font-size: 16px; font-size: 16px;
width: 16px; width: 16px;
height: 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 { .dlg-dot {
width: 4px; width: 3px;
height: 4px; height: 3px;
border-radius: 2px; border-radius: 50%;
background: rgba(0, 0, 0, 0.54); background: rgba(0, 0, 0, 0.38);
flex-shrink: 0;
}
.dlg-header-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0; flex-shrink: 0;
margin: 0 4px;
} }
.dlg-close { .dlg-close {
@ -274,13 +288,64 @@
.dlg-meta { .dlg-meta {
display: flex; display: flex;
gap: 24px; gap: 24px;
padding: 16px 24px; padding: 16px 24px 12px;
} }
.dlg-meta-group { .dlg-meta-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px;
}
.dlg-meta-creator {
display: flex;
align-items: center;
gap: 8px; 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 // Meta label Design: 12px Medium, rgba(0,0,0,0.54), tracking 0.25
@ -295,7 +360,30 @@
.dlg-meta-chips { .dlg-meta-chips {
display: flex; display: flex;
flex-wrap: wrap; 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 // Chip Design: 12px Medium, h-24, p-4 px-8, rounded-4
@ -550,7 +638,7 @@
.dlg-installed-badge { .dlg-installed-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
align-self: flex-start; flex-shrink: 0;
gap: 4px; gap: 4px;
padding: 4px 8px; padding: 4px 8px;
border-radius: 16px; 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 // Open details / Solution instructions link Design: 12px Medium, $tb-primary-color
.dlg-info-link { .dlg-info-link {
display: inline-flex; display: inline-flex;
@ -615,10 +726,11 @@
} }
} }
// Footer only Close button // Footer
.dlg-footer { .dlg-footer {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
padding: 8px; padding: 8px;
border-top: 1px solid rgba(0, 0, 0, 0.12); border-top: 1px solid rgba(0, 0, 0, 0.12);
flex-shrink: 0; flex-shrink: 0;

22
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 { export interface IotHubItemDetailDialogData {
item: MpItemVersionView; item: MpItemVersionView;
installedItem?: IotHubInstalledItem; installedItem?: IotHubInstalledItem;
installedItemsCount?: number;
mode?: IotHubItemDetailDialogMode; mode?: IotHubItemDetailDialogMode;
showCreator?: boolean; showCreator?: boolean;
} }
@ -58,6 +59,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
typeTranslations = itemTypeTranslations; typeTranslations = itemTypeTranslations;
readmeContent: string = ''; readmeContent: string = '';
installedItem?: IotHubInstalledItem; installedItem?: IotHubInstalledItem;
installedItemsCount = 0;
carouselImages: string[] = []; carouselImages: string[] = [];
carouselIndex = 0; carouselIndex = 0;
@ -75,6 +77,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
this.mode = data.mode || 'default'; this.mode = data.mode || 'default';
this.showCreator = data.showCreator !== false; this.showCreator = data.showCreator !== false;
this.installedItem = data.installedItem; this.installedItem = data.installedItem;
this.installedItemsCount = data.installedItemsCount || 0;
this.buildCarouselImages(); this.buildCarouselImages();
this.loadReadme(); this.loadReadme();
} }
@ -87,6 +90,10 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
return this.item.image ? this.iotHubApiService.resolveResourceUrl(this.item.image) : null; return this.item.image ? this.iotHubApiService.resolveResourceUrl(this.item.image) : null;
} }
getCreatorAvatarUrl(): string | null {
return this.item.creatorAvatarUrl ? this.iotHubApiService.resolveResourceUrl(this.item.creatorAvatarUrl) : null;
}
getTypeChipClass(): string { getTypeChipClass(): string {
switch (this.item.type) { switch (this.item.type) {
case ItemType.WIDGET: return 'type-widget'; case ItemType.WIDGET: return 'type-widget';
@ -232,10 +239,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
return this.installedItem != null; return this.installedItem != null;
} }
isSameVersion(): boolean {
return this.installedItem != null
&& this.installedItem.itemVersionId === this.item.id;
}
hasUpdate(): boolean { hasUpdate(): boolean {
return this.installedItem != null return this.installedItem != null
@ -243,10 +247,6 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
} }
install(): void { install(): void {
if (this.item.type === ItemType.DEVICE) {
this.installDevice();
return;
}
const dialogRef = this.dialog.open(TbIotHubInstallDialogComponent, { const dialogRef = this.dialog.open(TbIotHubInstallDialogComponent, {
panelClass: ['tb-dialog'], panelClass: ['tb-dialog'],
data: { data: {
@ -260,7 +260,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
}); });
} }
private installDevice(): void { installDevice(): void {
const versionId = this.item.id as string; const versionId = this.item.id as string;
this.iotHubApiService.getVersionFileData(versionId, { ignoreLoading: true }).subscribe({ this.iotHubApiService.getVersionFileData(versionId, { ignoreLoading: true }).subscribe({
next: async (blob: Blob) => { next: async (blob: Blob) => {
@ -373,6 +373,10 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
this.dialogRef.close({ action: 'add', item: this.item }); this.dialogRef.close({ action: 'add', item: this.item });
} }
openInstalledItemsDialog(): void {
// TODO: implement installed items dialog
}
goToPrevSlide(): void { goToPrevSlide(): void {
this.carouselIndex = (this.carouselIndex - 1 + this.carouselImages.length) % this.carouselImages.length; this.carouselIndex = (this.carouselIndex - 1 + this.carouselImages.length) % this.carouselImages.length;
} }

1
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.html

@ -126,6 +126,7 @@
<tb-iot-hub-item-card <tb-iot-hub-item-card
[item]="item" [item]="item"
[installedItem]="getInstalledItem(item)" [installedItem]="getInstalledItem(item)"
[installedItemsCount]="getInstalledItemsCount(item)"
[showTypeChip]="false" [showTypeChip]="false"
[showSubtype]="false" [showSubtype]="false"
[showCreator]="showCreator" [showCreator]="showCreator"

31
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts

@ -75,6 +75,10 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
installedWidgets: IotHubInstalledItem[] = []; installedWidgets: IotHubInstalledItem[] = [];
installedSolutionTemplates: IotHubInstalledItem[] = []; installedSolutionTemplates: IotHubInstalledItem[] = [];
installedDeviceCounts: Record<string, number> = {};
installedDashboardCounts: Record<string, number> = {};
installedCalcFieldCounts: Record<string, number> = {};
installedRuleChainCounts: Record<string, number> = {};
private searchSubject = new Subject<string>(); private searchSubject = new Subject<string>();
private searchSubscription: Subscription; 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 // Dialogs
openItemDetail(item: MpItemVersionView): void { 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') { if (result === 'installed' || result === 'deleted' || result === 'updated') {
this.reloadInstalledItems(); this.reloadInstalledItems();
} }
@ -286,10 +305,18 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
const pageLink = new PageLink(10000, 0); const pageLink = new PageLink(10000, 0);
forkJoin({ forkJoin({
widgets: this.iotHubApiService.getInstalledItems(pageLink, ItemType.WIDGET, config), 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 => { }).subscribe(results => {
this.installedWidgets = results.widgets.data; this.installedWidgets = results.widgets.data;
this.installedSolutionTemplates = results.solutionTemplates.data; this.installedSolutionTemplates = results.solutionTemplates.data;
this.installedDeviceCounts = results.deviceCounts;
this.installedDashboardCounts = results.dashboardCounts;
this.installedCalcFieldCounts = results.calcFieldCounts;
this.installedRuleChainCounts = results.ruleChainCounts;
}); });
} }

6
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) { @for (item of popularDevices; track item.id) {
<tb-iot-hub-item-card <tb-iot-hub-item-card
[item]="item" [item]="item"
[installedItem]="getInstalledDevice(item)" [installedItemsCount]="findInstalledItemsCount(item)"
[showTypeChip]="false" [showTypeChip]="false"
[showCreator]="true" [showCreator]="true"
(cardClick)="openItemDetail($event)" (cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)" (creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)" (installClick)="installItem($event)">
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card> </tb-iot-hub-item-card>
} }
</div> </div>

64
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[] = []; installedWidgets: IotHubInstalledItem[] = [];
installedSolutionTemplates: IotHubInstalledItem[] = []; installedSolutionTemplates: IotHubInstalledItem[] = [];
installedDevices: IotHubInstalledItem[] = []; installedDeviceCounts: Record<string, number> = {};
installedDashboardCounts: Record<string, number> = {};
installedCalcFieldCounts: Record<string, number> = {};
installedRuleChainCounts: Record<string, number> = {};
installedItemsCount = 0; installedItemsCount = 0;
isLoading = true; isLoading = true;
@ -310,7 +313,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
} }
openItemDetail(item: MpItemVersionView): void { 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') { if (result === 'installed' || result === 'deleted') {
this.reloadInstalledItems(item.type); this.reloadInstalledItems(item.type);
} }
@ -332,8 +335,20 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
this.installedSolutionTemplates = data.data; this.installedSolutionTemplates = data.data;
}); });
} else if (type === ItemType.DEVICE) { } else if (type === ItemType.DEVICE) {
this.iotHubApiService.getInstalledItems(pageLink, ItemType.DEVICE, config).subscribe(data => { this.iotHubApiService.getInstalledItemCounts(ItemType.DEVICE, config).subscribe(counts => {
this.installedDevices = data.data; 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); return this.installedWidgets.find(i => i.itemId === item.itemId);
case ItemType.SOLUTION_TEMPLATE: case ItemType.SOLUTION_TEMPLATE:
return this.installedSolutionTemplates.find(i => i.itemId === item.itemId); return this.installedSolutionTemplates.find(i => i.itemId === item.itemId);
case ItemType.DEVICE:
return this.installedDevices.find(i => i.itemId === item.itemId);
default: default:
return undefined; 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 { installItem(item: MpItemVersionView): void {
this.iotHubActions.installItem(item).subscribe(result => { this.iotHubActions.installItem(item).subscribe(result => {
if (result === 'installed') { if (result === 'installed') {
@ -381,10 +409,6 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
return this.installedSolutionTemplates.find(i => i.itemId === item.itemId); 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 { deleteInstalledItem(item: MpItemVersionView): void {
const installedItem = this.findInstalledItem(item); const installedItem = this.findInstalledItem(item);
if (!installedItem) { return; } 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); this.installedWidgets = this.installedWidgets.filter(i => i.id.id !== installedItem.id.id);
} else if (item.type === ItemType.SOLUTION_TEMPLATE) { } else if (item.type === ItemType.SOLUTION_TEMPLATE) {
this.installedSolutionTemplates = this.installedSolutionTemplates.filter(i => i.id.id !== installedItem.id.id); this.installedSolutionTemplates = this.installedSolutionTemplates.filter(i => i.id.id !== installedItem.id.id);
} else if (item.type === ItemType.DEVICE) { } else if (item.type === ItemType.DEVICE && this.installedDeviceCounts[item.itemId]) {
this.installedDevices = this.installedDevices.filter(i => i.id.id !== installedItem.id.id); 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), devices: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DEVICE, this.bigCardCount), config),
installedWidgets: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.WIDGET, 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),
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) installedCount: this.iotHubApiService.getInstalledItemsCount(null, config)
}).subscribe({ }).subscribe({
next: (results) => { next: (results) => {
@ -467,7 +500,10 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
this.popularDevices = results.devices.data; this.popularDevices = results.devices.data;
this.installedWidgets = results.installedWidgets.data; this.installedWidgets = results.installedWidgets.data;
this.installedSolutionTemplates = results.installedSolutionTemplates.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.installedItemsCount = results.installedCount;
this.isLoading = false; this.isLoading = false;
}, },

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

@ -3772,6 +3772,8 @@
"retry": "Retry", "retry": "Retry",
"no-items-found": "No items found", "no-items-found": "No items found",
"no-items-found-text": "Try adjusting your search or filters.", "no-items-found-text": "Try adjusting your search or filters.",
"creator": "Creator",
"manage": "Manage",
"become-a-creator": "Become a creator", "become-a-creator": "Become a creator",
"back-to-iot-hub": "Back to IoT Hub", "back-to-iot-hub": "Back to IoT Hub",
"verified-creator": "Verified Creator", "verified-creator": "Verified Creator",
@ -3799,6 +3801,7 @@
"install-type": "Type: {{type}}", "install-type": "Type: {{type}}",
"install-creator": "Creator: {{creator}}", "install-creator": "Creator: {{creator}}",
"install": "Install", "install": "Install",
"connect-device": "Connect device",
"install-one-more": "Install one more", "install-one-more": "Install one more",
"installing": "Installing...", "installing": "Installing...",
"installed": "Installed", "installed": "Installed",
@ -3815,6 +3818,7 @@
"updates": "Updates", "updates": "Updates",
"up-to-date": "Up to date", "up-to-date": "Up to date",
"update": "Update", "update": "Update",
"update-to-version": "Update to v {{ version }}",
"update-item-title": "Update Item", "update-item-title": "Update Item",
"update-confirm": "Update '{{name}}' to version {{version}}?", "update-confirm": "Update '{{name}}' to version {{version}}?",
"update-confirm-title": "Update '{{name}}'?", "update-confirm-title": "Update '{{name}}'?",

Loading…
Cancel
Save