Browse Source

refactor(iot-hub): centralise installed-item entity URL + selectedTab deep link

- 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.
pull/15772/head
Igor Kulikov 4 months ago
parent
commit
54a0b80d22
  1. 19
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts
  2. 2
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts
  3. 4
      ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts
  4. 50
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts
  5. 8
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts
  6. 42
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts
  7. 31
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
  8. 8
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts
  9. 8
      ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.ts
  10. 8
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts
  11. 8
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
  12. 8
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts
  13. 34
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts

19
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'); const id = paramMap.get('entityId');
this.currentEntityId = { id, entityType }; this.currentEntityId = { id, entityType };
this.reload(); 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;
} }
})); }));
} }

2
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 (entityTabs) {
if (this.viewInited) { if (this.viewInited) {
this.matTabGroup._tabs.reset([...this.inclusiveTabs.toArray(), ...entityTabs]); this.matTabGroup._tabs.reset([...this.inclusiveTabs.toArray(), ...entityTabs]);
this.matTabGroup.selectedIndex = this.selectedTab;
this.matTabGroup._tabs.notifyOnChanges(); this.matTabGroup._tabs.notifyOnChanges();
} else { } else {
this.pendingTabs = entityTabs; this.pendingTabs = entityTabs;
@ -321,6 +322,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV
this.viewInited = true; this.viewInited = true;
if (this.pendingTabs) { if (this.pendingTabs) {
this.matTabGroup._tabs.reset([...this.inclusiveTabs.toArray(), ...this.pendingTabs]); this.matTabGroup._tabs.reset([...this.inclusiveTabs.toArray(), ...this.pendingTabs]);
this.matTabGroup.selectedIndex = this.selectedTab;
this.matTabGroup._tabs.notifyOnChanges(); this.matTabGroup._tabs.notifyOnChanges();
this.pendingTabs = null; this.pendingTabs = null;
} }

4
ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts

@ -117,6 +117,10 @@ export abstract class EntityTabsComponent<T extends BaseData<HasId>,
); );
} }
resolveTabIndex(tab: string): number {
return 0;
}
protected setEntity(entity: T) { protected setEntity(entity: T) {
this.entityValue = entity; this.entityValue = entity;
} }

50
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts

@ -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<string, EntityType> = {
'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;
}

8
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 { DialogComponent } from '@shared/components/dialog.component';
import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.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 { IotHubApiService } from '@core/http/iot-hub-api.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id'; 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 { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
@ -256,7 +258,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
} }
} else { } else {
this.state = 'success'; this.state = 'success';
this.entityDetailsUrl = resolveEntityDetailsUrl(result.descriptor, this.item.type); this.entityDetailsUrl = getInstalledItemUrl(result.descriptor);
} }
} else { } else {
this.state = 'error'; this.state = 'error';

42
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts

@ -40,13 +40,12 @@ import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { import {
DeviceInstalledItemDescriptor, DeviceInstalledItemDescriptor,
getInstalledItemUrl,
IotHubInstalledItem, IotHubInstalledItem,
ItemPublishedVersionInfo ItemPublishedVersionInfo
} from '@shared/models/iot-hub/iot-hub-installed-item.models'; } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { EntityType } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils';
import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service';
@Component({ @Component({
@ -209,42 +208,11 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges,
}); });
} }
getEntityId(item: IotHubInstalledItem): string | null {
const descriptor = item.descriptor;
switch (descriptor.type) {
case 'WIDGET': return descriptor.widgetTypeId?.id;
case 'DASHBOARD': return descriptor.dashboardId?.id;
case 'CALCULATED_FIELD': return descriptor.entityId?.id;
case 'ALARM_RULE': return descriptor.entityId?.id;
case 'RULE_CHAIN': return descriptor.ruleChainId?.id;
case 'DEVICE': return descriptor.dashboardId?.id ?? null;
case 'SOLUTION_TEMPLATE': return descriptor.dashboardId?.id;
default: return null;
}
}
getEntityType(item: IotHubInstalledItem): EntityType | null {
const descriptor = item.descriptor;
switch (descriptor.type) {
case 'WIDGET': return EntityType.WIDGET_TYPE;
case 'DASHBOARD': return EntityType.DASHBOARD;
case 'CALCULATED_FIELD': return descriptor.entityId?.entityType as EntityType;
case 'ALARM_RULE': return descriptor.entityId?.entityType as EntityType;
case 'RULE_CHAIN': return EntityType.RULE_CHAIN;
case 'DEVICE': return descriptor.dashboardId ? EntityType.DASHBOARD : null;
case 'SOLUTION_TEMPLATE': return EntityType.DASHBOARD;
default: return null;
}
}
openEntity(item: IotHubInstalledItem): void { openEntity(item: IotHubInstalledItem): void {
const entityType = this.getEntityType(item); const url = getInstalledItemUrl(item?.descriptor);
const entityId = this.getEntityId(item); if (url) {
if (entityType && entityId) { const urlTree = this.router.parseUrl(url);
const url = getEntityDetailsPageURL(entityId, entityType); window.open(this.router.serializeUrl(urlTree), '_blank');
if (url) {
window.open(this.router.serializeUrl(this.router.parseUrl(url)), '_blank');
}
} }
} }

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

@ -22,11 +22,9 @@ import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, NodeInfo } from '@shared/models/iot-hub/iot-hub-version.models'; import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, NodeInfo } from '@shared/models/iot-hub/iot-hub-version.models';
import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { getInstalledItemUrl, IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils';
import { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component'; import { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component';
import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service';
@ -199,29 +197,10 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
} }
openEntityDetails(): void { openEntityDetails(): void {
const descriptor = this.installedItem?.descriptor; const url = getInstalledItemUrl(this.installedItem?.descriptor);
if (!descriptor) { if (url) {
return; this.dialogRef.close();
} void this.router.navigateByUrl(url);
let entityId: string | null = null;
let entityType: EntityType | 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;
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) {
const url = getEntityDetailsPageURL(entityId, entityType);
if (url) {
this.dialogRef.close();
void this.router.navigateByUrl(url);
}
} }
} }

8
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts

@ -21,11 +21,13 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.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 { IotHubApiService } from '@core/http/iot-hub-api.service';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { resolveEntityDetailsUrl } from './iot-hub-components.models';
import { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component'; import { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component';
export interface IotHubUpdateDialogData { export interface IotHubUpdateDialogData {
@ -86,7 +88,7 @@ export class TbIotHubUpdateDialogComponent extends DialogComponent<TbIotHubUpdat
}); });
} else { } else {
this.state = 'success'; this.state = 'success';
this.entityDetailsUrl = resolveEntityDetailsUrl(result.descriptor, this.data.itemType); this.entityDetailsUrl = getInstalledItemUrl(result.descriptor);
} }
} else if (result.entityModified) { } else if (result.entityModified) {
this.state = 'confirm'; this.state = 'confirm';

8
ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.ts

@ -36,4 +36,12 @@ export class AssetProfileTabsComponent extends EntityTabsComponent<AssetProfile>
super.ngOnInit(); super.ngOnInit();
} }
resolveTabIndex(tab: string): number {
if (tab === 'cf') {
return 1;
} else {
return super.resolveTabIndex(tab);
}
}
} }

8
ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts

@ -36,4 +36,12 @@ export class AssetTabsComponent extends EntityTabsComponent<AssetInfo> {
super.ngOnInit(); super.ngOnInit();
} }
resolveTabIndex(tab: string): number {
if (tab === 'cf') {
return 3;
} else {
return super.resolveTabIndex(tab);
}
}
} }

8
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts

@ -56,6 +56,14 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfil
}); });
} }
resolveTabIndex(tab: string): number {
if (tab === 'cf') {
return 2;
} else {
return super.resolveTabIndex(tab);
}
}
protected setEntity(entity: DeviceProfile) { protected setEntity(entity: DeviceProfile) {
this.isTransportTypeChanged = false; this.isTransportTypeChanged = false;
super.setEntity(entity); super.setEntity(entity);

8
ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts

@ -36,4 +36,12 @@ export class DeviceTabsComponent extends EntityTabsComponent<DeviceInfo> {
super.ngOnInit(); super.ngOnInit();
} }
resolveTabIndex(tab: string): number {
if (tab === 'cf') {
return 3;
} else {
return super.resolveTabIndex(tab);
}
}
} }

34
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 { BaseData } from '@shared/models/base-data';
import { EntityType } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils';
export interface WidgetInstalledItemDescriptor { export interface WidgetInstalledItemDescriptor {
type: 'WIDGET'; type: 'WIDGET';
@ -97,3 +99,35 @@ export interface IotHubInstalledItem extends BaseData<{id: string}> {
version: string; version: string;
descriptor: IotHubInstalledItemDescriptor; 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;
}

Loading…
Cancel
Save