From d2beb455971f475d5789de923665bdb3c8cbd097 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 3 Jun 2026 17:02:39 +0200 Subject: [PATCH 01/13] Fix "No data" overlay not hiding when widget data becomes available after empty state --- .../src/app/modules/home/components/widget/widget.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index fc7ad9f64b..058a178825 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -683,6 +683,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } private reInitImpl() { + this.displayNoData = false; this.onDestroy(); if (!this.typeParameters.useCustomDatasources) { this.createDefaultSubscription().subscribe({ @@ -726,7 +727,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds); } if (subscriptionChanged && !this.typeParameters.useCustomDatasources) { - this.displayNoData = false; this.reInit(); } } @@ -740,7 +740,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, subscriptionChanged = subscriptionChanged || subscription.onFiltersChanged(filterIds); } if (subscriptionChanged && !this.typeParameters.useCustomDatasources) { - this.displayNoData = false; this.reInit(); } } From 78cfbf4b32e9ecf0660ac217c75dd8f4c6fb5953 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 19 Jun 2026 18:27:30 +0300 Subject: [PATCH 02/13] fix(iot-hub): mobile detail-dialog stacks preview+description, hero shrinks on lt-sm, device install URL falls back to first created device Detail dialog non-CF/RC layout stacks the preview and description on lt-md instead of overflowing horizontally. Hero cluster moves up to 15% and title drops to 28px on the smallest viewport. getInstalledItemUrl for DEVICE descriptors now falls back to the first DEVICE entry in createdEntityIds when dashboardId is missing, so single-device packages still resolve to an entity URL. --- .../iot-hub-item-detail-dialog.component.html | 2 +- .../home/pages/iot-hub/iot-hub-home.component.scss | 8 ++++++++ .../models/iot-hub/iot-hub-installed-item.models.ts | 12 ++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) 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 e42f1521aa..30d9322dd9 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 @@ -96,7 +96,7 @@ } @else { -
+
@if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss index f0768bf330..c433bad1a9 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss @@ -757,6 +757,14 @@ padding: 140px 16px 40px; } + .tb-iot-hub-hero-cluster { + top: 15%; + } + + .tb-iot-hub-hero-title { + font-size: 28px; + } + .tb-iot-hub-categories { grid-template-columns: 1fr; } diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts index b2320453bb..b2bb0e7ad9 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts @@ -140,8 +140,16 @@ export const getInstalledItemUrl = (descriptor?: IotHubInstalledItemDescriptor): let query: string | null = null; switch (descriptor.type) { case 'DEVICE': - entityId = descriptor.dashboardId?.id; - entityType = EntityType.DASHBOARD; + if (descriptor.dashboardId) { + entityId = descriptor.dashboardId?.id; + entityType = EntityType.DASHBOARD; + } else if (descriptor.createdEntityIds) { + const found = descriptor.createdEntityIds.find(id => id.entityType === EntityType.DEVICE); + if (found) { + entityId = found.id; + entityType = EntityType.DEVICE; + } + } break; case 'WIDGET': entityId = descriptor.widgetTypeId?.id; From f5a1052a480f612e98fe6cde204e02e9fc1329c2 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 19 Jun 2026 20:05:17 +0300 Subject: [PATCH 03/13] 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 04/13] 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 05/13] 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 06/13] 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 2e60dd32e651caf0331d2534576136e0924e62bc Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 22 Jun 2026 17:29:04 +0300 Subject: [PATCH 07/13] style(dashboard): bump widget-select title row min-height to 42px to fit close-button outline --- .../dashboard-page/dashboard-widget-select.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss index d77a929fd8..c63eb26caf 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss @@ -50,7 +50,7 @@ tb-dashboard-widget-select { .tb-widget-select-title-row { gap: 8px; width: 100%; - min-height: 40px; + min-height: 42px; .tb-widget-select-title { font-size: 24px; From 61ce61bf5c4504755593784a606ef2a8cc2b1092 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 23 Jun 2026 11:37:26 +0300 Subject: [PATCH 08/13] refactor(iot-hub): move installed-item delete API call into the delete dialog Push the deleteInstalledItem call from IotHubActionsService into the dialog's confirm() handler so the dialog itself manages its lifetime. Pass installedItemId in the dialog data and have confirm() fire the API call, then close with true on success. Drop the now-unused isLoading flips in the installed-items table since loading state is no longer optimistically toggled around the dialog. --- .../components/iot-hub/iot-hub-actions.service.ts | 10 ++-------- .../iot-hub/iot-hub-delete-dialog.component.ts | 12 ++++++++++-- .../iot-hub-installed-items-table.component.ts | 6 ------ 3 files changed, 12 insertions(+), 16 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 702eb6b1d9..d26565ee95 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,6 @@ import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Observable, of, EMPTY } from 'rxjs'; -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'; @@ -120,13 +119,8 @@ export class IotHubActionsService { panelClass: ['tb-dialog'], disableClose: true, autoFocus: false, - data: { itemName: installedItem.itemName, itemType: installedItem.itemType } - }).afterClosed().pipe( - mergeMap((confirmed) => - confirmed - ? this.iotHubApiService.deleteInstalledItem(installedItem.id.id).pipe(map(() => true)) - : of(false) ) - ); + data: { installedItemId: installedItem.id.id, itemName: installedItem.itemName, itemType: installedItem.itemType } + }).afterClosed(); } installDevice(item: MpItemVersionView): Observable { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.ts index 4353f1a9f6..be91644741 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.ts @@ -20,8 +20,11 @@ import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; +import { IotHubInstalledItem } from 'src/app/shared/models/iot-hub/iot-hub-installed-item.models'; +import { IotHubApiService } from '@core/http/iot-hub-api.service'; export interface IotHubDeleteDialogData { + installedItemId: string; itemName: string; itemType?: string; } @@ -38,13 +41,18 @@ export class TbIotHubDeleteDialogComponent extends DialogComponent, protected router: Router, protected dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: IotHubDeleteDialogData + @Inject(MAT_DIALOG_DATA) public data: IotHubDeleteDialogData, + private iotHubApiService: IotHubApiService ) { super(store, router, dialogRef); } confirm(): void { - this.dialogRef.close(true); + this.iotHubApiService.deleteInstalledItem(this.data.installedItemId).subscribe( + () => { + this.dialogRef.close(true); + } + ); } cancel(): void { 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 281d1d1e0e..4435c82736 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,12 +156,9 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges, } deleteItem(item: IotHubInstalledItem): void { - this.isLoading = true; this.iotHubActions.deleteItem(item).subscribe(deleted => { if (deleted) { this.loadData(); - } else { - this.isLoading = false; } }); } @@ -247,12 +244,9 @@ 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; } }); } From 48e70f9b0ddd83b7e34dca930ba6609cee65f061 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 23 Jun 2026 11:41:24 +0300 Subject: [PATCH 09/13] chore(iot-hub): drop unused IotHubApiService dependency from IotHubActionsService --- .../modules/home/components/iot-hub/iot-hub-actions.service.ts | 2 -- 1 file changed, 2 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 d26565ee95..73e627fbe6 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 @@ -22,7 +22,6 @@ import { DialogService } from '@core/services/dialog.service'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; import { DeviceInstalledItemDescriptor, IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; -import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { EntityId } from '@shared/models/id/entity-id'; import { TbIotHubAddItemDialogComponent, IotHubAddItemDialogData, IotHubAddItemDialogResult } from './iot-hub-add-item-dialog.component'; import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData, IotHubItemDetailDialogMode } from './iot-hub-item-detail-dialog.component'; @@ -37,7 +36,6 @@ export class IotHubActionsService { constructor( private dialog: MatDialog, - private iotHubApiService: IotHubApiService, private dialogService: DialogService, private translate: TranslateService ) {} From 23433497abbbf725b080efda79f127d0859f4014 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Thu, 25 Jun 2026 14:39:47 +0300 Subject: [PATCH 10/13] IoT Hub install report (#15838) * feat: send pseudonymized install report (tenant/user hash, tb version) to IoT Hub * refactor: source install-report tb version/edition from ProjectInfo Use the existing ProjectInfo component (getProjectVersion/getProductType) instead of an ad-hoc optional BuildProperties field and a hard-coded "CE" edition, so PE reports its own product type and the version is the canonical cleaned value. Constructor-injected via @RequiredArgsConstructor. --- .../service/iot_hub/DefaultIotHubService.java | 23 +++++++--- .../server/service/iot_hub/InstallReport.java | 19 ++++++++ .../service/iot_hub/IotHubRestClient.java | 4 +- .../service/iot_hub/InstallReportTest.java | 46 +++++++++++++++++++ 4 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java create mode 100644 application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java index e88d812f5b..f7cc08391a 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -64,6 +64,7 @@ import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; import org.thingsboard.server.service.entitiy.device.TbDeviceService; import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService; import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; +import org.thingsboard.server.service.install.ProjectInfo; import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -112,6 +113,7 @@ public class DefaultIotHubService implements IotHubService { private final DeviceService deviceService; private final TbDeviceService tbDeviceService; private final SolutionService solutionService; + private final ProjectInfo projectInfo; // Field names of the marketplace version JSON payload. Both the install path and the // install-plan resolver parse the same shape, so the contract lives here in one place. @@ -196,10 +198,11 @@ public class DefaultIotHubService implements IotHubService { } try { - iotHubRestClient.reportVersionInstalled(versionId); + InstallReport report = buildInstallReport(tenantId.getId(), user.getId().getId(), + projectInfo.getProjectVersion(), projectInfo.getProductType()); + iotHubRestClient.reportVersionInstalled(versionId, report); } catch (Exception e) { - // Counter ping is best-effort — do not fail the install if it errors. - log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage()); + log.warn("[{}] Failed to report install for version {}: {}", tenantId, versionId, e.getMessage()); } log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType); return installedItem; @@ -611,6 +614,13 @@ public class DefaultIotHubService implements IotHubService { } } + static InstallReport buildInstallReport(UUID tenantId, UUID userId, String tbVersion, String edition) { + String salt = tenantId.toString(); + String tenantHash = sha256(salt + tenantId); + String userHash = sha256(salt + userId); + return new InstallReport(tenantHash, userHash, tbVersion, edition); + } + @Override public InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor) { TenantId tenantId = user.getTenantId(); @@ -650,10 +660,11 @@ public class DefaultIotHubService implements IotHubService { } try { - iotHubRestClient.reportVersionInstalled(versionId); + InstallReport report = buildInstallReport(tenantId.getId(), user.getId().getId(), + projectInfo.getProjectVersion(), projectInfo.getProductType()); + iotHubRestClient.reportVersionInstalled(versionId, report); } catch (Exception e) { - // Counter ping is best-effort — do not fail the install if it errors. - log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage()); + log.warn("[{}] Failed to report install for version {}: {}", tenantId, versionId, e.getMessage()); } log.info("[{}] Registered device package install: {} (version {})", tenantId, itemName, version); diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java new file mode 100644 index 0000000000..3b253e426c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.iot_hub; + +public record InstallReport(String tenantHash, String userHash, String tbVersion, String edition) { +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java index c67fef55a8..ae3cf5b07e 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java @@ -120,9 +120,9 @@ public class IotHubRestClient { }); } - public void reportVersionInstalled(String versionId) { + public void reportVersionInstalled(String versionId, InstallReport report) { String url = baseUrl + "/api/versions/" + versionId + "/install"; log.debug("Reporting IoT Hub version installed: {}", url); - restTemplate.postForObject(url, null, Void.class); + restTemplate.postForObject(url, report, Void.class); } } diff --git a/application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java b/application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java new file mode 100644 index 0000000000..73f968fc01 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.iot_hub; + +import org.junit.jupiter.api.Test; + +import java.security.MessageDigest; +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class InstallReportTest { + + private static String sha256(String s) throws Exception { + MessageDigest d = MessageDigest.getInstance("SHA-256"); + return HexFormat.of().formatHex(d.digest(s.getBytes(StandardCharsets.UTF_8))); + } + + @Test + void buildInstallReport_hashesMatchSaltedFormula() throws Exception { + UUID tenantId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + + InstallReport r = DefaultIotHubService.buildInstallReport(tenantId, userId, "4.2.0", "CE"); + + assertEquals(sha256(tenantId.toString() + tenantId), r.tenantHash()); + assertEquals(sha256(tenantId.toString() + userId), r.userHash()); + assertEquals("4.2.0", r.tbVersion()); + assertEquals("CE", r.edition()); + } +} From d95724fd5e3d3b86aca6b3f49f51122c36e46bd4 Mon Sep 17 00:00:00 2001 From: Daria Shevchenko <116559345+dashevchenko@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:17:04 +0300 Subject: [PATCH 11/13] [Alarm comment] Fixed alarm comment deletion button visibility for admin (#15837) * fixed alarm comment deletion button visibility for admin * docs(alarm): point comment-permission note at the backend check it mirrors --- .../alarm/alarm-comment.component.html | 4 +++- .../components/alarm/alarm-comment.component.ts | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html index 25db54641d..0e5b9977ca 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html @@ -69,7 +69,7 @@