]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: migrated host table tabs to resource pages 69136/head
authorSyed Ali Ul Hasan <syedaliulhasan19@gmail.com>
Sat, 6 Jun 2026 16:47:23 +0000 (22:17 +0530)
committerSyed Ali Ul Hasan <syedaliulhasan19@gmail.com>
Wed, 10 Jun 2026 17:38:29 +0000 (23:08 +0530)
Fixes :  https://tracker.ceph.com/issues/76712
Signed-off-by: Syed Ali Ul Hasan <syedaliulhasan19@gmail.com>
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-breadcrumb.resolver.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-section.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details-section.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts

index 0e41636c19dd0242db639840e88076dbdffbbebb..5a77bb1f1dba452bced0be966576f9fd3586d560 100644 (file)
@@ -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,
index a711704ad6ec556bafae24f509c67dfde38186ad..990bd0d1f15214e4c3b4773e7f4f4572306c0ade 100644 (file)
@@ -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 (file)
index 0000000..fe27747
--- /dev/null
@@ -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 (file)
index 0000000..1c91b7d
--- /dev/null
@@ -0,0 +1,31 @@
+@if (hostname) {
+  @switch (section) {
+    @case ('devices') {
+      <cd-device-list [hostname]="hostname"></cd-device-list>
+    }
+    @case ('physical-disks') {
+      <cd-inventory [hostname]="hostname"></cd-inventory>
+    }
+    @case ('daemons') {
+      <cd-service-daemon-list [hostname]="hostname"
+                              flag="hostDetails"
+                              [hiddenColumns]="['hostname']">
+      </cd-service-daemon-list>
+    }
+    @case ('performance-details') {
+      <cd-grafana i18n-title
+                  title="Host details"
+                  [grafanaPath]="'ceph-host-details?var-ceph_hosts=' + hostname"
+                  [type]="'metrics'"
+                  uid="rtOg0AiWz"
+                  grafanaStyle="five">
+      </cd-grafana>
+    }
+    @case ('device-health') {
+      <cd-smart-list [hostname]="hostname"></cd-smart-list>
+    }
+  }
+} @else {
+  <cd-alert-panel type="error"
+                  i18n>No hostname found.</cd-alert-panel>
+}
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 (file)
index 0000000..c4e92dd
--- /dev/null
@@ -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'] ?? '';
+  }
+}
index e2c838a608303f6bc86b981a222b2e31cf141cf7..35343d1f505d3d98e1f56c79e5e56ce99f2e6d15 100644 (file)
@@ -1,62 +1,4 @@
-<ng-container *ngIf="selection">
-  <nav ngbNav
-       #nav="ngbNav"
-       class="nav-tabs"
-       cdStatefulTab="host-details">
-    <ng-container ngbNavItem="devices">
-      <a ngbNavLink
-         i18n>Devices</a>
-      <ng-template ngbNavContent>
-        <cd-device-list [hostname]="selection['hostname']"></cd-device-list>
-      </ng-template>
-    </ng-container>
-    <ng-container ngbNavItem="inventory"
-                  *ngIf="permissions.hosts.read">
-      <a ngbNavLink
-         i18n>Physical Disks</a>
-      <ng-template ngbNavContent>
-        <cd-inventory [hostname]="selectedHostname"></cd-inventory>
-      </ng-template>
-    </ng-container>
-    <ng-container ngbNavItem="daemons"
-                  *ngIf="permissions.hosts.read">
-      <a ngbNavLink
-         i18n>Daemons</a>
-      <ng-template ngbNavContent>
-        <cd-service-daemon-list [hostname]="selectedHostname"
-                                flag="hostDetails"
-                                [hiddenColumns]="['hostname']">
-        </cd-service-daemon-list>
-      </ng-template>
-    </ng-container>
-    <ng-container ngbNavItem="performance-details"
-                  *ngIf="permissions.grafana.read">
-      <a ngbNavLink
-         i18n>Performance Details</a>
-      <ng-template ngbNavContent>
-        <cd-grafana i18n-title
-                    title="Host details"
-                    [grafanaPath]="'ceph-host-details?var-ceph_hosts=' + selectedHostname"
-                    [type]="'metrics'"
-                    uid="rtOg0AiWz"
-                    grafanaStyle="five">
-        </cd-grafana>
-      </ng-template>
-    </ng-container>
-    <ng-container ngbNavItem="device-health">
-      <a ngbNavLink
-         i18n>Device health</a>
-      <ng-template ngbNavContent>
-        <cd-smart-list *ngIf="selectedHostname; else noHostname"
-                       [hostname]="selectedHostname"></cd-smart-list>
-      </ng-template>
-    </ng-container>
-  </nav>
-
-  <div [ngbNavOutlet]="nav"></div>
-</ng-container>
-
-<ng-template #noHostname>
-  <cd-alert-panel type="error"
-                  i18n>No hostname found.</cd-alert-panel>
-</ng-template>
+<cd-sidebar-layout
+  [title]="hostname"
+  [items]="sidebarItems"
+></cd-sidebar-layout>
index 59c6666e3747a39014e74f6aba8b0cc041117df1..f0086af43651883ce3018eab224fb91797b7a068 100644 (file)
@@ -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');
+    });
   });
 });
index 9af6a87a52fa93ba8ce535c4be2d0e8ca78b3875..9384acdca68e74deb7b8875c3e8b9170d10c3ff2 100644 (file)
@@ -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;
   }
 }
index 9ea40658b1bef35e9e825d0d84ba92dbf307ff82..8f85391c66630a4a9f7c8b1b25503c1247419d4b 100644 (file)
                 (fetchData)="getHosts($event)"
                 selectionType="single"
                 [searchableObjects]="true"
-                [hasDetails]="hasTableDetails"
                 [serverSide]="true"
                 [count]="count"
                 [maxLimit]="25"
-                (setExpandedRow)="setExpandedRow($event)"
                 (updateSelection)="updateSelection($event)"
                 [toolHeader]="!hideToolHeader">
         <div class="table-actions">
                             [tableActions]="expandClusterActions">
           </cd-table-actions>
         </div>
-        <cd-host-details *cdTableDetail
-                         [permissions]="permissions"
-                         [selection]="expandedRow">
-        </cd-host-details>
       </cd-table>
     </ng-template>
   </ng-container>
 <ng-template #hostNameTpl
              let-row="data.row">
   <span data-testid="hostname">
-    {{ row.hostname }}
+    <a cdsLink
+       [routerLink]="[viewUrl, row.hostname | encodeUri]"
+       (click)="$event.stopPropagation()">
+      {{ row.hostname }}
+    </a>
   </span><br>
   <span class="text-muted fst-italic"
         *ngIf="row.addr"
index d6f21651e0a3c89e9a7a64b2e5592c6af9941234..07d29d635360537b2ef6440133a97373dab58cb9 100644 (file)
@@ -104,6 +104,7 @@ export class HostsComponent extends ListWithDetails implements OnDestroy, OnInit
   icons = Icons;
   private tableContext: CdTableFetchDataContext = null;
   count = 5;
+  viewUrl = '/hosts';
 
   messages = {
     nonOrchHost: $localize`The feature is disabled because the selected host is not managed by Orchestrator.`