From f5a1052a480f612e98fe6cde204e02e9fc1329c2 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 19 Jun 2026 20:05:17 +0300 Subject: [PATCH 1/5] fix(iot-hub): tighten delete/update result handling and switch button spinners/icons to matButtonIcon IotHubActionsService.deleteItem now returns true/false (no cancel-as-EMPTY) and updateItem returns string|boolean; callers no longer early-return on a null installed item and instead let the service produce a false, so the subscriber consistently fires. Installed-items table shows a translucent loading overlay during update/delete via isLoading + relative-positioned container. Replace mr-2 inline-block align-middle hacks on dialog button spinners with matButtonIcon, and use matButtonIcon for the check-for-updates button icon/spinner too. --- .../iot-hub/iot-hub-actions.service.ts | 21 +++++++------ .../iot-hub/iot-hub-browse.component.ts | 10 +++--- .../iot-hub-install-dialog.component.html | 10 +++--- ...t-hub-installed-items-table.component.html | 17 +++++----- ...t-hub-installed-items-table.component.scss | 1 + ...iot-hub-installed-items-table.component.ts | 10 ++++-- .../iot-hub-item-detail-dialog.component.ts | 4 +-- .../iot-hub/iot-hub-search.component.ts | 8 ++--- .../iot-hub-update-dialog.component.html | 2 +- .../iot-hub-update-dialog.component.ts | 4 +-- .../pages/iot-hub/iot-hub-home.component.ts | 31 ++++++++++--------- .../iot-hub-installed-items.component.html | 4 +-- 12 files changed, 67 insertions(+), 55 deletions(-) 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 bae080a464..dd0b89598f 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 @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Observable, of, EMPTY } from 'rxjs'; -import { filter, mergeMap } from 'rxjs/operators'; +import { filter, map, mergeMap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { DialogService } from '@core/services/dialog.service'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; @@ -94,11 +94,11 @@ export class IotHubActionsService { }).afterClosed(); } - updateItem(installedItem: IotHubInstalledItem, version: string, versionId: string): Observable { + updateItem(installedItem: IotHubInstalledItem, version: string, versionId: string): Observable { if (!installedItem) { - return EMPTY; + return of(false); } - return this.dialog.open(TbIotHubUpdateDialogComponent, { + return this.dialog.open(TbIotHubUpdateDialogComponent, { panelClass: ['tb-dialog'], disableClose: true, autoFocus: false, @@ -108,7 +108,7 @@ export class IotHubActionsService { itemType: installedItem.itemType as ItemType, version, versionId - } as IotHubUpdateDialogData + } }).afterClosed(); } @@ -116,15 +116,16 @@ export class IotHubActionsService { if (!installedItem) { return of(false); } - return this.dialog.open(TbIotHubDeleteDialogComponent, { + return this.dialog.open(TbIotHubDeleteDialogComponent, { panelClass: ['tb-dialog'], disableClose: true, autoFocus: false, - data: { itemName: installedItem.itemName, itemType: installedItem.itemType } as IotHubDeleteDialogData + data: { itemName: installedItem.itemName, itemType: installedItem.itemType } }).afterClosed().pipe( - filter(confirmed => !!confirmed), - mergeMap(() => this.iotHubApiService.deleteInstalledItem(installedItem.id.id)), - mergeMap(() => of(true)) + mergeMap((confirmed) => + confirmed + ? this.iotHubApiService.deleteInstalledItem(installedItem.id.id).pipe(map(() => true)) + : of(false) ) ); } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts index 2efea716bf..5954ebc49f 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts @@ -580,8 +580,7 @@ export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy updateItem(item: MpItemVersionView): void { const installedItem = this.getInstalledItem(item); - if (!installedItem) { return; } - this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => { + this.iotHubActions.updateItem(installedItem, item.version, item.id).subscribe(result => { if (result === 'updated') { this.reloadInstalledItems(); } @@ -590,9 +589,10 @@ export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy deleteInstalledItem(item: MpItemVersionView): void { const installedItem = this.getInstalledItem(item); - if (!installedItem) { return; } - this.iotHubActions.deleteItem(installedItem).subscribe(() => { - this.reloadInstalledItems(); + this.iotHubActions.deleteItem(installedItem).subscribe((deleted) => { + if (deleted) { + this.reloadInstalledItems(); + } }); } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html index bb551a4106..ea3774ac11 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html @@ -157,7 +157,7 @@ @@ -169,7 +169,7 @@ [disabled]="ruleChainInstallForm.invalid || resolvingPlan" (click)="onRuleChainInstall()"> @if (resolvingPlan) { - + } {{ 'iot-hub.install' | translate }} @@ -178,7 +178,7 @@ [disabled]="(activeSelectEntityConfig.required && !selectedEntityId) || resolvingPlan" (click)="onEntitySelectInstall()"> @if (resolvingPlan) { - + } {{ 'iot-hub.install' | translate }} @@ -188,7 +188,7 @@ @@ -204,7 +204,7 @@ @case ('installing') { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html index a64f9ab73e..11143dc0bf 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html @@ -135,14 +135,8 @@ - @if (isLoading) { -
- -
- } - @if (!isLoading && dataSource.length === 0) { -
+

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

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

@@ -166,4 +160,13 @@ (page)="onPageChange($event)" [showFirstLastButtons]="true"> + + @if (isLoading) { +
+
+ +
+
+ } +
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss index 400bb5778b..a908470ae8 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss @@ -25,6 +25,7 @@ // Table + paginator container — border .tb-installed-table-container { + position: relative; border: 1px solid rgba(0, 0, 0, 0.12); } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts index bb5e1ecd7a..281d1d1e0e 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts @@ -156,9 +156,12 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges, } deleteItem(item: IotHubInstalledItem): void { - this.iotHubActions.deleteItem(item).subscribe(confirmed => { - if (confirmed) { + this.isLoading = true; + this.iotHubActions.deleteItem(item).subscribe(deleted => { + if (deleted) { this.loadData(); + } else { + this.isLoading = false; } }); } @@ -244,9 +247,12 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges, } updateItem(item: IotHubInstalledItem, publishedInfo: ItemPublishedVersionInfo): void { + this.isLoading = true; this.iotHubActions.updateItem(item, publishedInfo.publishedVersion, publishedInfo.publishedVersionId).subscribe(result => { if (result === 'updated') { this.loadData(); + } else { + this.isLoading = false; } }); } 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 04d49b7f86..bc72b7a94c 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 @@ -220,8 +220,8 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent { - if (confirmed) { + this.iotHubActions.deleteItem(this.installedItem).subscribe(deleted => { + if (deleted) { this.dialogRef.close('deleted'); } }); diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts index 64ff558723..790baf05f7 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts @@ -247,7 +247,6 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { updateItem(item: MpItemVersionView): void { const installedItem = this.getInstalledItem(item); - if (!installedItem) { return; } this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => { if (result === 'updated') { this.reloadInstalledItems(); @@ -257,9 +256,10 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { deleteInstalledItem(item: MpItemVersionView): void { const installedItem = this.getInstalledItem(item); - if (!installedItem) { return; } - this.iotHubActions.deleteItem(installedItem).subscribe(() => { - this.reloadInstalledItems(); + this.iotHubActions.deleteItem(installedItem).subscribe((deleted) => { + if (deleted) { + this.reloadInstalledItems(); + } }); } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html index 78e8fc3b66..48396089ca 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html @@ -51,7 +51,7 @@ @case ('updating') { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts index 6962073104..67ef8c14f9 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts @@ -46,7 +46,7 @@ export type UpdateState = 'confirm' | 'updating' | 'success' | 'error'; templateUrl: './iot-hub-update-dialog.component.html', styleUrls: ['./iot-hub-install-dialog.component.scss'] }) -export class TbIotHubUpdateDialogComponent extends DialogComponent { +export class TbIotHubUpdateDialogComponent extends DialogComponent { ItemType = ItemType; @@ -58,7 +58,7 @@ export class TbIotHubUpdateDialogComponent extends DialogComponent, protected router: Router, - protected dialogRef: MatDialogRef, + protected dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: IotHubUpdateDialogData, private dialog: MatDialog, private dialogService: DialogService, 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 87f8eee0b2..07c3b701eb 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 @@ -381,7 +381,6 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { updateItem(item: MpItemVersionView): void { const installedItem = this.findInstalledItem(item); - if (!installedItem) { return; } this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => { if (result === 'updated') { this.reloadInstalledItems(item.type); @@ -404,20 +403,22 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { deleteInstalledItem(item: MpItemVersionView): void { const installedItem = this.findInstalledItem(item); if (!installedItem) { return; } - this.iotHubActions.deleteItem(installedItem).subscribe(() => { - this.installedItemsCount = Math.max(0, this.installedItemsCount - 1); - if (item.type === ItemType.WIDGET) { - this.installedWidgets = this.installedWidgets.filter(i => i.id.id !== installedItem.id.id); - } else if (item.type === ItemType.SOLUTION_TEMPLATE) { - this.installedSolutionTemplates = this.installedSolutionTemplates.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.CALCULATED_FIELD && this.installedCalcFieldCounts[item.itemId]) { - this.installedCalcFieldCounts[item.itemId] = Math.max(0, this.installedCalcFieldCounts[item.itemId] - 1); - } else if (item.type === ItemType.ALARM_RULE && this.installedAlarmRuleCounts[item.itemId]) { - this.installedAlarmRuleCounts[item.itemId] = Math.max(0, this.installedAlarmRuleCounts[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); + this.iotHubActions.deleteItem(installedItem).subscribe((deleted) => { + if (deleted) { + this.installedItemsCount = Math.max(0, this.installedItemsCount - 1); + if (item.type === ItemType.WIDGET) { + this.installedWidgets = this.installedWidgets.filter(i => i.id.id !== installedItem.id.id); + } else if (item.type === ItemType.SOLUTION_TEMPLATE) { + this.installedSolutionTemplates = this.installedSolutionTemplates.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.CALCULATED_FIELD && this.installedCalcFieldCounts[item.itemId]) { + this.installedCalcFieldCounts[item.itemId] = Math.max(0, this.installedCalcFieldCounts[item.itemId] - 1); + } else if (item.type === ItemType.ALARM_RULE && this.installedAlarmRuleCounts[item.itemId]) { + this.installedAlarmRuleCounts[item.itemId] = Math.max(0, this.installedAlarmRuleCounts[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); + } } }); } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html index be6793f766..1061eb07a9 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html @@ -28,9 +28,9 @@ @if (!updatesChecked) { From d3bc589a510b995fbcaa8ae635974ec4915ba84e Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 19 Jun 2026 20:10:39 +0300 Subject: [PATCH 2/5] chore(iot-hub): drop now-unused filter import from IotHubActionsService --- .../modules/home/components/iot-hub/iot-hub-actions.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dd0b89598f..702eb6b1d9 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 @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Observable, of, EMPTY } from 'rxjs'; -import { filter, map, mergeMap } from 'rxjs/operators'; +import { map, mergeMap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { DialogService } from '@core/services/dialog.service'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; From 0db943f4422e6f65546c1b2a09c5715769067b3c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 22 Jun 2026 14:46:20 +0300 Subject: [PATCH 3/5] fix(iot-hub): let active-filters row grow when chips wrap so card grid doesn't overlap PROD-8420: .tb-iot-hub-sort-row was locked to height: 32px, so when many use-case filters were selected the chip-set wrapped to multiple lines but the row stayed one line tall and the card grid below rendered on top of the wrapped chips. Switch to min-height: 32px so the row keeps its baseline single-line height when empty but grows naturally with wrapped chips. --- .../home/components/iot-hub/iot-hub-browse.component.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss index fc685f5b90..1f04befb6a 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss @@ -359,14 +359,17 @@ } } -// Filter chips row — Design: 32px tall, no padding, centered .tb-iot-hub-sort-row { - height: 32px; + min-height: 32px; align-items: center; padding: 0; margin-top: 16px; margin-bottom: 12px; + ::ng-deep .mat-mdc-chip-set .mdc-evolution-chip-set__chips { + row-gap: 8px; + } + .mat-mdc-chip { margin-top: 0; margin-bottom: 0; From 304904cedb8ca006f8a06fe50880af5d9bed25cd Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 22 Jun 2026 14:46:23 +0300 Subject: [PATCH 4/5] fix(home): reset main-content scroll on route navigation so pages open at the top PROD-8423: .tb-main-content (the scroll container holding the router-outlet in home.component.html) keeps its scrollTop across route changes, so navigating from a scrolled-down IoT Hub home to a child page (e.g. Calculated Fields) opened the new page already scrolled. Angular's scrollPositionRestoration only handles window scroll, not arbitrary containers. Add a #mainContent ref, capture the ElementRef, and reset its scroll position in the existing (activate) handler so every route navigation lands at the top. --- ui-ngx/src/app/modules/home/home.component.html | 2 +- ui-ngx/src/app/modules/home/home.component.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/home.component.html b/ui-ngx/src/app/modules/home/home.component.html index 10c15080f6..03ce291f18 100644 --- a/ui-ngx/src/app/modules/home/home.component.html +++ b/ui-ngx/src/app/modules/home/home.component.html @@ -79,7 +79,7 @@ -
+
diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts index 6191280933..1bd9b73386 100644 --- a/ui-ngx/src/app/modules/home/home.component.ts +++ b/ui-ngx/src/app/modules/home/home.component.ts @@ -58,6 +58,8 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni @ViewChild('sidenav') sidenav: MatSidenav; + @ViewChild('mainContent', { static: true }) mainContent: ElementRef; + @ViewChild('searchInput') searchInputField: ElementRef; fullscreenEnabled = screenfull.isEnabled; @@ -136,6 +138,7 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni activeComponentChanged(activeComponent: any) { this.activeComponentService.setCurrentActiveComponent(activeComponent); + this.mainContent?.nativeElement?.scrollTo({ top: 0, left: 0 }); if (!this.activeComponent) { setTimeout(() => { this.updateActiveComponent(activeComponent); From a8846fad62cb8116137863a68b83d99a7b7ce013 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 22 Jun 2026 15:04:03 +0300 Subject: [PATCH 5/5] fix(iot-hub): surface dashboard save errors during device-package install/overwrite Drop ignoreErrors:true on the two saveDashboard calls in the device-install dialog so backend failures (validation errors, missing permissions, entity-limit hits) surface to the global interceptor instead of being swallowed and reported as a generic "install failed" with no detail. --- .../device-install-dialog/device-install-dialog.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts index 98fa33514f..9a8ef0802c 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts @@ -884,7 +884,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent