From 15e47f930d52b2c6539fd39b04306577bf0f86df Mon Sep 17 00:00:00 2001 From: Syed Ali Ul Hasan Date: Sat, 6 Jun 2026 22:17:23 +0530 Subject: [PATCH] mgr/dashboard: migrated host table tabs to resource pages Fixes : https://tracker.ceph.com/issues/76712 Signed-off-by: Syed Ali Ul Hasan --- .../frontend/src/app/app-routing.module.ts | 36 +++++++++ .../src/app/ceph/cluster/cluster.module.ts | 2 + .../host-details-breadcrumb.resolver.ts | 17 +++++ .../host-details-section.component.html | 31 ++++++++ .../host-details-section.component.ts | 21 ++++++ .../host-details/host-details.component.html | 66 +--------------- .../host-details.component.spec.ts | 41 ++++++---- .../host-details/host-details.component.ts | 75 ++++++++++++++++--- .../ceph/cluster/hosts/hosts.component.html | 12 ++- .../app/ceph/cluster/hosts/hosts.component.ts | 1 + 10 files changed, 211 insertions(+), 91 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-breadcrumb.resolver.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-section.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-section.component.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 0e41636c19d..5a77bb1f1db 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -8,7 +8,10 @@ import { ConfigurationFormComponent } from './ceph/cluster/configuration/configu import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component'; import { CreateClusterComponent } from './ceph/cluster/create-cluster/create-cluster.component'; import { CrushmapComponent } from './ceph/cluster/crushmap/crushmap.component'; +import { HostDetailsComponent } from './ceph/cluster/hosts/host-details/host-details.component'; import { HostFormComponent } from './ceph/cluster/hosts/host-form/host-form.component'; +import { HostDetailsBreadcrumbResolver } from './ceph/cluster/hosts/host-details/host-details-breadcrumb.resolver'; +import { HostDetailsSectionComponent } from './ceph/cluster/hosts/host-details/host-details-section.component'; import { HostsComponent } from './ceph/cluster/hosts/hosts.component'; import { InventoryComponent } from './ceph/cluster/inventory/inventory.component'; import { LogsComponent } from './ceph/cluster/logs/logs.component'; @@ -153,6 +156,39 @@ const routes: Routes = [ } ] }, + { + path: 'hosts/:hostname', + component: HostDetailsComponent, + data: { breadcrumbs: HostDetailsBreadcrumbResolver }, + children: [ + { path: '', redirectTo: 'devices', pathMatch: 'full' }, + { + path: 'devices', + component: HostDetailsSectionComponent, + data: { breadcrumbs: 'Devices', section: 'devices' } + }, + { + path: 'physical-disks', + component: HostDetailsSectionComponent, + data: { breadcrumbs: 'Physical Disks', section: 'physical-disks' } + }, + { + path: 'daemons', + component: HostDetailsSectionComponent, + data: { breadcrumbs: 'Daemons', section: 'daemons' } + }, + { + path: 'performance-details', + component: HostDetailsSectionComponent, + data: { breadcrumbs: 'Performance Details', section: 'performance-details' } + }, + { + path: 'device-health', + component: HostDetailsSectionComponent, + data: { breadcrumbs: 'Device health', section: 'device-health' } + } + ] + }, { path: 'ceph-users', component: CRUDTableComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index a711704ad6e..990bd0d1f15 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -60,6 +60,7 @@ import { CreateClusterStep4Component } from './create-cluster/create-cluster-ste import { CreateClusterComponent } from './create-cluster/create-cluster.component'; import { CrushmapComponent } from './crushmap/crushmap.component'; import { HostDetailsComponent } from './hosts/host-details/host-details.component'; +import { HostDetailsSectionComponent } from './hosts/host-details/host-details-section.component'; import { HostFormComponent } from './hosts/host-form/host-form.component'; import { HostsComponent } from './hosts/hosts.component'; import { InventoryDevicesComponent } from './inventory/inventory-devices/inventory-devices.component'; @@ -155,6 +156,7 @@ import { TextLabelListComponent } from '~/app/shared/components/text-label-list/ OsdScrubModalComponent, OsdFlagsModalComponent, HostDetailsComponent, + HostDetailsSectionComponent, ConfigurationDetailsComponent, ConfigurationFormComponent, OsdReweightModalComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-breadcrumb.resolver.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-breadcrumb.resolver.ts new file mode 100644 index 00000000000..fe277471be4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-breadcrumb.resolver.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs'; + +@Injectable({ + providedIn: 'root' +}) +export class HostDetailsBreadcrumbResolver extends BreadcrumbsResolver { + resolve(route: ActivatedRouteSnapshot): IBreadcrumb[] { + const hostname = route.parent?.params?.hostname || route.params?.hostname || ''; + return [ + { text: 'Cluster/Hosts', path: '/hosts' }, + { text: hostname, path: this.getFullPath(route) } + ]; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-section.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-section.component.html new file mode 100644 index 00000000000..1c91b7deead --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-section.component.html @@ -0,0 +1,31 @@ +@if (hostname) { + @switch (section) { + @case ('devices') { + + } + @case ('physical-disks') { + + } + @case ('daemons') { + + + } + @case ('performance-details') { + + + } + @case ('device-health') { + + } + } +} @else { + No hostname found. +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-section.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-section.component.ts new file mode 100644 index 00000000000..c4e92ddc853 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-section.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; + +@Component({ + selector: 'cd-host-details-section', + templateUrl: './host-details-section.component.html', + standalone: false +}) +export class HostDetailsSectionComponent implements OnInit { + hostname = ''; + section = ''; + + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + this.route.parent?.paramMap.subscribe((pm: ParamMap) => { + this.hostname = pm.get('hostname') ?? ''; + }); + this.section = this.route.snapshot.data['section'] ?? ''; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html index e2c838a6083..35343d1f505 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html @@ -1,62 +1,4 @@ - - - -
-
- - - No hostname found. - + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts index 59c6666e374..f0086af4365 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts @@ -1,14 +1,17 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, convertToParamMap } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; import { CephModule } from '~/app/ceph/ceph.module'; import { CephSharedModule } from '~/app/ceph/shared/ceph-shared.module'; import { CoreModule } from '~/app/core/core.module'; import { Permissions } from '~/app/shared/models/permissions'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { SharedModule } from '~/app/shared/shared.module'; -import { configureTestBed, TabHelper } from '~/testing/unit-test-helper'; +import { configureTestBed } from '~/testing/unit-test-helper'; import { HostDetailsComponent } from './host-details.component'; describe('HostDetailsComponent', () => { @@ -24,36 +27,44 @@ describe('HostDetailsComponent', () => { CoreModule, CephSharedModule, SharedModule + ], + providers: [ + { + provide: ActivatedRoute, + useValue: { + paramMap: of(convertToParamMap({ hostname: 'localhost' })) + } + }, + { + provide: AuthStorageService, + useValue: { + getPermissions: () => new Permissions({ hosts: ['read'], grafana: ['read'] }) + } + } ] }); beforeEach(() => { fixture = TestBed.createComponent(HostDetailsComponent); component = fixture.componentInstance; - component.selection = undefined; - component.permissions = new Permissions({ - hosts: ['read'], - grafana: ['read'] - }); }); it('should create', () => { expect(component).toBeTruthy(); }); - describe('Host details tabset', () => { + describe('Host resource layout', () => { beforeEach(() => { - component.selection = { hostname: 'localhost' }; fixture.detectChanges(); }); - it('should recognize a tabset child', () => { - const tabsetChild = TabHelper.getNgbNav(fixture); - expect(tabsetChild).toBeDefined(); + it('should render the sidebar layout', () => { + const layout = fixture.nativeElement.querySelector('cd-sidebar-layout'); + expect(layout).toBeTruthy(); }); - it('should show tabs', () => { - expect(TabHelper.getTextContents(fixture)).toEqual([ + it('should build the sidebar items', () => { + expect(component.sidebarItems.map((item) => item.label)).toEqual([ 'Devices', 'Physical Disks', 'Daemons', @@ -61,5 +72,9 @@ describe('HostDetailsComponent', () => { 'Device health' ]); }); + + it('should set the hostname title', () => { + expect(component.hostname).toBe('localhost'); + }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts index 9af6a87a52f..9384acdca68 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts @@ -1,21 +1,78 @@ -import { Component, Input } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; -import { Permissions } from '~/app/shared/models/permissions'; +import { Subscription } from 'rxjs'; + +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { SidebarItem } from '~/app/shared/components/sidebar-layout/sidebar-layout.component'; @Component({ selector: 'cd-host-details', templateUrl: './host-details.component.html', styleUrls: ['./host-details.component.scss'], + encapsulation: ViewEncapsulation.None, standalone: false }) -export class HostDetailsComponent { - @Input() - permissions: Permissions; +export class HostDetailsComponent implements OnInit, OnDestroy { + private sub = new Subscription(); + public readonly basePath = '/hosts'; + hostname = ''; + sidebarItems: SidebarItem[] = []; + + constructor(private route: ActivatedRoute, private authStorageService: AuthStorageService) {} + + ngOnInit(): void { + const permissions = this.authStorageService.getPermissions(); + this.sub.add( + this.route.paramMap.subscribe((pm: ParamMap) => { + this.hostname = pm.get('hostname') ?? ''; + this.buildSidebarItems(permissions); + }) + ); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + private buildSidebarItems(permissions: any): void { + const items: SidebarItem[] = [ + { + label: $localize`Devices`, + route: [this.basePath, this.hostname, 'devices'], + routerLinkActiveOptions: { exact: true } + } + ]; + + if (permissions.hosts?.read) { + items.push( + { + label: $localize`Physical Disks`, + route: [this.basePath, this.hostname, 'physical-disks'], + routerLinkActiveOptions: { exact: true } + }, + { + label: $localize`Daemons`, + route: [this.basePath, this.hostname, 'daemons'], + routerLinkActiveOptions: { exact: true } + } + ); + } + + if (permissions.grafana?.read) { + items.push({ + label: $localize`Performance Details`, + route: [this.basePath, this.hostname, 'performance-details'], + routerLinkActiveOptions: { exact: true } + }); + } - @Input() - selection: any; + items.push({ + label: $localize`Device health`, + route: [this.basePath, this.hostname, 'device-health'], + routerLinkActiveOptions: { exact: true } + }); - get selectedHostname(): string { - return this.selection !== undefined ? this.selection['hostname'] : null; + this.sidebarItems = items; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html index 9ea40658b1b..8f85391c666 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html @@ -12,11 +12,9 @@ (fetchData)="getHosts($event)" selectionType="single" [searchableObjects]="true" - [hasDetails]="hasTableDetails" [serverSide]="true" [count]="count" [maxLimit]="25" - (setExpandedRow)="setExpandedRow($event)" (updateSelection)="updateSelection($event)" [toolHeader]="!hideToolHeader">
@@ -33,10 +31,6 @@ [tableActions]="expandClusterActions">
- - @@ -73,7 +67,11 @@ - {{ row.hostname }} + + {{ row.hostname }} +