From 54a0b80d22ebaa5ba719bdc3f6652658b56cb6f5 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Jun 2026 11:06:13 +0300 Subject: [PATCH 1/2] refactor(iot-hub): centralise installed-item entity URL + selectedTab deep link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a single getInstalledItemUrl(descriptor) helper to iot-hub-installed-item.models that maps every descriptor type (WIDGET / DASHBOARD / CALCULATED_FIELD / ALARM_RULE / RULE_CHAIN / SOLUTION_TEMPLATE) to its entity-details URL via the shared getEntityDetailsPageURL utility, appending `?selectedTab=cf` for calculated-field installs so the entity tabs page lands on the CF tab on first paint. - Drop the parallel resolveEntityDetailsUrl helper + iot-hub-components.models.ts file and rewire every installed-item open path (install dialog, update dialog, installed-items table, item detail dialog) to call getInstalledItemUrl. Installed-items table openEntity collapses to parseUrl + serializeUrl on the helper's output now that any selectedTab is part of the URL. - Teach EntityDetailsPageComponent to honour a `selectedTab=…` query param: read it from the current route on entityId change, strip it (replaceUrl) so refresh doesn't keep jumping the tab, then resolve a tab index via the new EntityTabsComponent.resolveTabIndex(tab) hook. Asset, device, asset-profile, and device-profile tabs override the hook to map the `cf` shortcut to their respective Calculated Fields tab position; other entities fall back to index 0. - EntityDetailsPanelComponent now applies this.selectedTab to the underlying MatTabGroup both when the entity-tabs query list refreshes and after view init, so the initial tab matches the resolved index instead of staying on 0. --- .../entity/entity-details-page.component.ts | 19 ++++++- .../entity/entity-details-panel.component.ts | 2 + .../entity/entity-tabs.component.ts | 4 ++ .../iot-hub/iot-hub-components.models.ts | 50 ------------------- .../iot-hub-install-dialog.component.ts | 8 +-- ...iot-hub-installed-items-table.component.ts | 42 ++-------------- .../iot-hub-item-detail-dialog.component.ts | 31 ++---------- .../iot-hub-update-dialog.component.ts | 8 +-- .../asset-profile-tabs.component.ts | 8 +++ .../home/pages/asset/asset-tabs.component.ts | 8 +++ .../device-profile-tabs.component.ts | 8 +++ .../pages/device/device-tabs.component.ts | 8 +++ .../iot-hub/iot-hub-installed-item.models.ts | 34 +++++++++++++ 13 files changed, 110 insertions(+), 120 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts index 8988a02380..7d9b9e07f7 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts @@ -98,7 +98,24 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl const id = paramMap.get('entityId'); this.currentEntityId = { id, entityType }; this.reload(); - this.selectedTab = 0; + const queryParams = this.route.snapshot.queryParams; + let selectedTabIndex = 0; + if (queryParams['selectedTab']) { + this.router.navigate([], { + queryParams: { + selectedTab: null + }, + queryParamsHandling: 'merge', + replaceUrl: true + }); + if (this.entityTabsComponent) { + const selectedTab: string = queryParams['selectedTab']; + if (selectedTab) { + selectedTabIndex = this.entityTabsComponent.resolveTabIndex(selectedTab); + } + } + } + this.selectedTab = selectedTabIndex; } })); } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index 8c6fc004d8..b9ed698a47 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -215,6 +215,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV if (entityTabs) { if (this.viewInited) { this.matTabGroup._tabs.reset([...this.inclusiveTabs.toArray(), ...entityTabs]); + this.matTabGroup.selectedIndex = this.selectedTab; this.matTabGroup._tabs.notifyOnChanges(); } else { this.pendingTabs = entityTabs; @@ -321,6 +322,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV this.viewInited = true; if (this.pendingTabs) { this.matTabGroup._tabs.reset([...this.inclusiveTabs.toArray(), ...this.pendingTabs]); + this.matTabGroup.selectedIndex = this.selectedTab; this.matTabGroup._tabs.notifyOnChanges(); this.pendingTabs = null; } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts index ec6e46fb72..c5b2eebeb4 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts @@ -117,6 +117,10 @@ export abstract class EntityTabsComponent, ); } + resolveTabIndex(tab: string): number { + return 0; + } + protected setEntity(entity: T) { this.entityValue = entity; } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts deleted file mode 100644 index 1233dfa89c..0000000000 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts +++ /dev/null @@ -1,50 +0,0 @@ -/// -/// 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. -/// - -import { EntityType } from '@shared/models/entity-type.models'; -import { IotHubInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; -import { getEntityDetailsPageURL } from '@core/utils'; - -export const ITEM_TYPE_TO_ENTITY_TYPE: Record = { - 'WIDGET': EntityType.WIDGET_TYPE, - 'DASHBOARD': EntityType.DASHBOARD, - 'CALCULATED_FIELD': EntityType.CALCULATED_FIELD, - 'ALARM_RULE': EntityType.CALCULATED_FIELD, - 'RULE_CHAIN': EntityType.RULE_CHAIN, - 'DEVICE': EntityType.DEVICE_PROFILE -}; - -export function resolveEntityDetailsUrl(descriptor: IotHubInstalledItemDescriptor, itemType: string): string | null { - if (!descriptor) { - return null; - } - const entityType = ITEM_TYPE_TO_ENTITY_TYPE[itemType]; - if (!entityType) { - return null; - } - let entityId: string | null = null; - switch (descriptor.type) { - case 'WIDGET': entityId = descriptor.widgetTypeId?.id; break; - case 'DASHBOARD': entityId = descriptor.dashboardId?.id; break; - case 'CALCULATED_FIELD': entityId = descriptor.calculatedFieldId?.id; break; - case 'ALARM_RULE': entityId = descriptor.calculatedFieldId?.id; break; - case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; break; - } - if (!entityId) { - return null; - } - return getEntityDetailsPageURL(entityId, entityType) || null; -} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts index 6484a32c4b..34056abbfe 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts @@ -24,12 +24,14 @@ import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; -import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; +import { + getInstalledItemUrl, + SolutionTemplateInstalledItemDescriptor +} from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; import { EntityId } from '@shared/models/id/entity-id'; -import { resolveEntityDetailsUrl } from './iot-hub-components.models'; import { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; @@ -256,7 +258,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent super.ngOnInit(); } + resolveTabIndex(tab: string): number { + if (tab === 'cf') { + return 1; + } else { + return super.resolveTabIndex(tab); + } + } + } diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts index 10ef434755..03aa7a1fa1 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts @@ -36,4 +36,12 @@ export class AssetTabsComponent extends EntityTabsComponent { super.ngOnInit(); } + resolveTabIndex(tab: string): number { + if (tab === 'cf') { + return 3; + } else { + return super.resolveTabIndex(tab); + } + } + } diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts index 1a762ac881..bfefdf5d4c 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts @@ -56,6 +56,14 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent { super.ngOnInit(); } + resolveTabIndex(tab: string): number { + if (tab === 'cf') { + return 3; + } else { + return super.resolveTabIndex(tab); + } + } + } 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 25ac09d161..b46bdb2829 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 @@ -15,6 +15,8 @@ /// import { BaseData } from '@shared/models/base-data'; +import { EntityType } from '@shared/models/entity-type.models'; +import { getEntityDetailsPageURL } from '@core/utils'; export interface WidgetInstalledItemDescriptor { type: 'WIDGET'; @@ -97,3 +99,35 @@ export interface IotHubInstalledItem extends BaseData<{id: string}> { version: string; descriptor: IotHubInstalledItemDescriptor; } + +export const getInstalledItemUrl = (descriptor?: IotHubInstalledItemDescriptor): string | null => { + let entityId: string | null = null; + let entityType: EntityType | null = null; + let query: string | null = null; + switch (descriptor.type) { + case 'WIDGET': entityId = descriptor.widgetTypeId?.id; entityType = EntityType.WIDGET_TYPE; break; + case 'DASHBOARD': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break; + case 'CALCULATED_FIELD': + case 'ALARM_RULE': + entityId = descriptor.entityId?.id; + entityType = descriptor.entityId?.entityType as EntityType; + if (descriptor.type === 'CALCULATED_FIELD') { + query = 'selectedTab=cf'; + } + break; + case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; entityType = EntityType.RULE_CHAIN; break; + case 'SOLUTION_TEMPLATE': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break; + } + if (entityType && entityId) { + let url = getEntityDetailsPageURL(entityId, entityType); + if (url) { + if (query) { + url = `${url}?${query}`; + } + return url; + } else { + return null; + } + } + return null; +} From 8861bc6c9089965c7659a95557b134ea86df30c4 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Jun 2026 11:15:56 +0300 Subject: [PATCH 2/2] style(iot-hub): polish getInstalledItemUrl readability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Guard the helper against a missing descriptor by short-circuiting to null up front instead of letting the switch crash on `undefined.type`. - Expand the inline single-line `case 'X': … break;` arms in the switch so each branch reads on its own line, matching the formatting of the already-multi-line CALCULATED_FIELD / ALARM_RULE arms. --- .../iot-hub/iot-hub-installed-item.models.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) 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 b46bdb2829..3bdaa87e66 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 @@ -101,12 +101,21 @@ export interface IotHubInstalledItem extends BaseData<{id: string}> { } export const getInstalledItemUrl = (descriptor?: IotHubInstalledItemDescriptor): string | null => { + if (!descriptor) { + return null; + } let entityId: string | null = null; let entityType: EntityType | null = null; let query: string | null = null; switch (descriptor.type) { - case 'WIDGET': entityId = descriptor.widgetTypeId?.id; entityType = EntityType.WIDGET_TYPE; break; - case 'DASHBOARD': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break; + case 'WIDGET': + entityId = descriptor.widgetTypeId?.id; + entityType = EntityType.WIDGET_TYPE; + break; + case 'DASHBOARD': + entityId = descriptor.dashboardId?.id; + entityType = EntityType.DASHBOARD; + break; case 'CALCULATED_FIELD': case 'ALARM_RULE': entityId = descriptor.entityId?.id; @@ -115,8 +124,14 @@ export const getInstalledItemUrl = (descriptor?: IotHubInstalledItemDescriptor): query = 'selectedTab=cf'; } break; - case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; entityType = EntityType.RULE_CHAIN; break; - case 'SOLUTION_TEMPLATE': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break; + case 'RULE_CHAIN': + entityId = descriptor.ruleChainId?.id; + entityType = EntityType.RULE_CHAIN; + break; + case 'SOLUTION_TEMPLATE': + entityId = descriptor.dashboardId?.id; + entityType = EntityType.DASHBOARD; + break; } if (entityType && entityId) { let url = getEntityDetailsPageURL(entityId, entityType);