]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Breadcrumb should allow going back to subsystem tab
authorpujaoshahu <pshahu@redhat.com>
Wed, 4 Mar 2026 08:32:54 +0000 (14:02 +0530)
committerAfreen Misbah <afreen@ibm.com>
Wed, 29 Apr 2026 07:42:25 +0000 (13:12 +0530)
Fixes: https://tracker.ceph.com/issues/75288
Signed-off-by: pujaoshahu <pshahu@redhat.com>
(cherry picked from commit 69a7c6bf151a1e1d256b15c4dd1e5c590145005e)

12 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-form/nvmeof-namespaces-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.ts [new file with mode: 0644]

index cefe4268930d4864990d78d243d69590fb88056d..56e694a26bbde0b08341272792af42f15c177991 100644 (file)
@@ -101,6 +101,7 @@ import { NvmeofSubsystemOverviewComponent } from './nvmeof-subsystem-overview/nv
 import { NvmeSubsystemViewBreadcrumbResolver } from './nvme-subsystem-view/nvme-subsystem-view-breadcrumb.resolver';
 import { NvmeSubsystemViewComponent } from './nvme-subsystem-view/nvme-subsystem-view.component';
 import { NvmeofSubsystemPerformanceComponent } from './nvmeof-subsystem-performance/nvmeof-subsystem-performance.component';
+import { NvmeofTabsComponent } from './nvmeof-tabs/nvmeof-tabs.component';
 
 @NgModule({
   imports: [
@@ -186,7 +187,8 @@ import { NvmeofSubsystemPerformanceComponent } from './nvmeof-subsystem-performa
     NvmeofEditHostKeyModalComponent,
     NvmeofSubsystemsStepFourComponent,
     NvmeofSubsystemOverviewComponent,
-    NvmeofSubsystemPerformanceComponent
+    NvmeofSubsystemPerformanceComponent,
+    NvmeofTabsComponent
   ],
 
   exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
@@ -345,123 +347,137 @@ const routes: Routes = [
       { path: '', redirectTo: 'gateways', pathMatch: 'full' },
       {
         path: 'gateways',
-        component: NvmeofGatewayComponent,
         data: { breadcrumbs: 'Gateways' },
         children: [
           {
-            path: `${URLVerbs.EDIT}/:subsystem_nqn/namespace/:nsid`,
-            component: NvmeofNamespaceExpandModalComponent,
-            outlet: 'modal'
-          }
-        ]
-      },
-      {
-        path: `gateways/${URLVerbs.CREATE}`,
-        component: NvmeofGroupFormComponent,
-        data: { breadcrumbs: `${ActionLabels.CREATE}${URLVerbs.GATEWAY_GROUP}` }
-      },
-
-      {
-        path: `gateways/${URLVerbs.VIEW}/:group`,
-        component: NvmeGatewayViewComponent,
-        data: { breadcrumbs: NvmeGatewayViewBreadcrumbResolver }, // Use resolver here
-        children: [
-          { path: '', redirectTo: 'nodes', pathMatch: 'full' },
+            path: '',
+            component: NvmeofGatewayGroupComponent
+          },
           {
-            path: 'nodes',
-            component: NvmeofGatewayNodeComponent,
-            data: { breadcrumbs: $localize`Gateway nodes`, mode: NvmeofGatewayNodeMode.DETAILS }
+            path: URLVerbs.CREATE,
+            component: NvmeofGroupFormComponent,
+            data: {
+              breadcrumbs: ActionLabels.CREATE,
+              pageHeader: {
+                title: $localize`Create Gateway Group`,
+                description: $localize`A logical group of gateways that hosts will connect to.`
+              }
+            }
           },
           {
-            path: 'subsystems',
-            component: NvmeofGatewaySubsystemComponent,
-            data: { breadcrumbs: $localize`Subsystems` }
+            path: `${URLVerbs.VIEW}/:group`,
+            component: NvmeGatewayViewComponent,
+            data: { breadcrumbs: NvmeGatewayViewBreadcrumbResolver },
+            children: [
+              { path: '', redirectTo: 'nodes', pathMatch: 'full' },
+              {
+                path: 'nodes',
+                component: NvmeofGatewayNodeComponent,
+                data: { breadcrumbs: $localize`Gateway nodes`, mode: NvmeofGatewayNodeMode.DETAILS }
+              },
+              {
+                path: 'subsystems',
+                component: NvmeofGatewaySubsystemComponent,
+                data: { breadcrumbs: $localize`Subsystems` }
+              }
+            ]
           }
         ]
       },
-      {
-        path: `namespaces/${URLVerbs.CREATE}`,
-        component: NvmeofNamespacesFormComponent,
-        data: { breadcrumbs: ActionLabels.CREATE + ' ' + $localize`Namespace` }
-      },
       {
         path: 'subsystems',
-        component: NvmeofSubsystemsComponent,
         data: { breadcrumbs: 'Subsystems' },
         children: [
-          // subsystems
-
-          {
-            path: URLVerbs.CREATE,
-            component: NvmeofSubsystemsFormComponent,
-            outlet: 'modal'
-          },
-          // listeners
-          {
-            path: `${URLVerbs.CREATE}/:subsystem_nqn/listener`,
-            component: NvmeofListenersFormComponent,
-            outlet: 'modal'
-          },
-          // namespaces
-          {
-            path: `${URLVerbs.CREATE}/:subsystem_nqn/namespace`,
-            component: NvmeofNamespacesFormComponent,
-            data: { breadcrumbs: ActionLabels.CREATE + ' ' + $localize`Namespace` }
-          },
           {
-            path: `${URLVerbs.EDIT}/:subsystem_nqn/namespace/:nsid`,
-            component: NvmeofNamespaceExpandModalComponent,
-            outlet: 'modal'
+            path: '',
+            component: NvmeofSubsystemsComponent,
+            children: [
+              {
+                path: URLVerbs.CREATE,
+                component: NvmeofSubsystemsFormComponent,
+                outlet: 'modal'
+              },
+              {
+                path: `${URLVerbs.CREATE}/:subsystem_nqn/listener`,
+                component: NvmeofListenersFormComponent,
+                outlet: 'modal'
+              },
+              {
+                path: `${URLVerbs.EDIT}/:subsystem_nqn/namespace/:nsid`,
+                component: NvmeofNamespaceExpandModalComponent,
+                outlet: 'modal'
+              },
+              {
+                path: `${URLVerbs.ADD}/:subsystem_nqn/initiator`,
+                component: NvmeofInitiatorsFormComponent,
+                outlet: 'modal'
+              }
+            ]
           },
-          // initiators
           {
-            path: `${URLVerbs.ADD}/:subsystem_nqn/initiator`,
-            component: NvmeofInitiatorsFormComponent,
-            outlet: 'modal'
+            path: ':subsystem_nqn',
+            component: NvmeSubsystemViewComponent,
+            data: { breadcrumbs: NvmeSubsystemViewBreadcrumbResolver },
+            children: [
+              { path: '', redirectTo: 'overview', pathMatch: 'full' },
+              {
+                path: 'overview',
+                component: NvmeofSubsystemOverviewComponent
+              },
+              {
+                path: 'hosts',
+                component: NvmeofInitiatorsListComponent
+              },
+              {
+                path: 'namespaces',
+                component: NvmeofSubsystemNamespacesListComponent
+              },
+              {
+                path: 'listeners',
+                component: NvmeofListenersListComponent
+              },
+              {
+                path: 'performance',
+                component: NvmeofSubsystemPerformanceComponent
+              },
+              {
+                path: `${URLVerbs.ADD}/initiator`,
+                component: NvmeofInitiatorsFormComponent,
+                outlet: 'modal'
+              },
+              {
+                path: `${URLVerbs.ADD}/listener`,
+                component: NvmeofListenersFormComponent,
+                outlet: 'modal'
+              },
+              {
+                path: `${URLVerbs.EDIT}/:subsystem_nqn/namespace/:nsid`,
+                component: NvmeofNamespaceExpandModalComponent,
+                outlet: 'modal'
+              }
+            ]
           }
         ]
       },
       {
-        path: `subsystems/:subsystem_nqn`,
-        component: NvmeSubsystemViewComponent,
-        data: { breadcrumbs: NvmeSubsystemViewBreadcrumbResolver },
+        path: 'namespaces',
+        data: { breadcrumbs: 'Namespaces' },
         children: [
-          { path: '', redirectTo: 'overview', pathMatch: 'full' },
-          {
-            path: 'overview',
-            component: NvmeofSubsystemOverviewComponent
-          },
-          {
-            path: 'hosts',
-            component: NvmeofInitiatorsListComponent
-          },
-
-          {
-            path: 'namespaces',
-            component: NvmeofSubsystemNamespacesListComponent
-          },
           {
-            path: 'listeners',
-            component: NvmeofListenersListComponent
+            path: '',
+            component: NvmeofNamespacesListComponent,
+            children: [
+              {
+                path: `${URLVerbs.EDIT}/:subsystem_nqn/namespace/:nsid`,
+                component: NvmeofNamespaceExpandModalComponent,
+                outlet: 'modal'
+              }
+            ]
           },
           {
-            path: 'performance',
-            component: NvmeofSubsystemPerformanceComponent
-          },
-          {
-            path: `${URLVerbs.ADD}/initiator`,
-            component: NvmeofInitiatorsFormComponent,
-            outlet: 'modal'
-          },
-          {
-            path: `${URLVerbs.ADD}/listener`,
-            component: NvmeofListenersFormComponent,
-            outlet: 'modal'
-          },
-          {
-            path: `${URLVerbs.EDIT}/:subsystem_nqn/namespace/:nsid`,
-            component: NvmeofNamespaceExpandModalComponent,
-            outlet: 'modal'
+            path: URLVerbs.CREATE,
+            component: NvmeofNamespacesFormComponent,
+            data: { breadcrumbs: ActionLabels.CREATE }
           }
         ]
       }
index 6b65039006dc7308f0ed185a4f82f70952b920d8..047d879699c533d02e2e9f6e43b00adbe28651cd 100644 (file)
@@ -43,6 +43,8 @@ export class NvmeofGatewayComponent implements OnInit, OnDestroy {
     this.route.queryParams.subscribe((params) => {
       if (params['tab'] && Object.values(TABS).includes(params['tab'])) {
         this.activeTab = params['tab'] as TABS;
+      } else {
+        this.activeTab = TABS.gateways;
       }
       this.breadcrumbService.setTabCrumb(TAB_LABELS[this.activeTab]);
     });
index 942051ef37abf57d5d9a49e3767d2f018ac7a810..9d8587bebe9ec21593cdaae34303487005045eca 100644 (file)
          [columnNumbers]="{sm: 4, md: 8}">
       <div cdsRow
            class="form-heading form-item">
-      <h3>{{ action | titlecase }} {{ resource }}</h3>
-      <cd-help-text>
-        <span i18n>
-           A logical group of gateways that hosts will connect to.
-        </span>
-      </cd-help-text>
       <cd-help-text [formAllFieldsRequired]="true"></cd-help-text>
       </div>
       <div cdsRow
index 79b9d6e68626369715fa68c660b5edd036725865..4f95496dd5836fb3cffa6b41ccdc64a7e8553765 100644 (file)
@@ -73,7 +73,7 @@ export class NvmeofNamespacesFormComponent implements OnInit {
     this.permission = this.authStorageService.getPermissions().nvmeof;
     this.poolPermission = this.authStorageService.getPermissions().pool;
     this.resource = $localize`Namespace`;
-    this.pageURL = 'block/nvmeof/gateways';
+    this.pageURL = 'block/nvmeof/namespaces';
   }
 
   init() {
@@ -91,7 +91,7 @@ export class NvmeofNamespacesFormComponent implements OnInit {
         this.group = params['group'];
       }
       if (this.subsystemNQN && this.group) {
-        this.pageURL = `block/nvmeof/subsystems/${this.subsystemNQN}/${this.group}`;
+        this.pageURL = `block/nvmeof/subsystems/${this.subsystemNQN}/namespaces`;
         this.action = this.actionLabels.ADD;
         this.title = this.action + ' ' + this.resource;
         this.description = $localize`Create a new namespace associated with this subsystem.`;
@@ -435,7 +435,7 @@ export class NvmeofNamespacesFormComponent implements OnInit {
       },
       complete: () => {
         this.router.navigate([this.pageURL], {
-          queryParams: { group: this.group, tab: 'namespace' }
+          queryParams: { group: this.group }
         });
       }
     });
index dde00bf70112282e9c63f941c145f2a1c3c8f129..ef76b9223fa46b5cb154d03df8bae11d921c04a5 100644 (file)
@@ -1,3 +1,5 @@
+<cd-nvmeof-tabs></cd-nvmeof-tabs>
+
 <div cdsGrid
      [useCssGrid]="true"
      [narrow]="true"
@@ -44,3 +46,5 @@
   </div>
 </cd-table>
 </ng-container>
+
+<router-outlet name="modal"></router-outlet>
index 5f5cbeccef48414221f7f7a59c01c6cd69c38825..e4a74bd917fc0193aebac83cd8483493f414dfee 100644 (file)
@@ -204,8 +204,8 @@ export class NvmeofSubsystemsFormComponent implements OnInit {
             errorMsg
           );
           this.isSubmitLoading = false;
-          this.router.navigate(['block/nvmeof/gateways'], {
-            queryParams: { group: this.group, tab: 'subsystem' }
+          this.router.navigate(['block/nvmeof/subsystems'], {
+            queryParams: { group: this.group }
           });
         }
       });
@@ -250,10 +250,9 @@ export class NvmeofSubsystemsFormComponent implements OnInit {
       : $localize`Subsystem created`;
 
     this.notificationService.show(type, title, sanitizedHtml);
-    this.router.navigate(['block/nvmeof/gateways'], {
+    this.router.navigate(['block/nvmeof/subsystems'], {
       queryParams: {
         group: this.group,
-        tab: 'subsystem',
         nqn: stepResults[0]?.success ? this.lastCreatedNqn : null
       }
     });
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.html
new file mode 100644 (file)
index 0000000..9b33888
--- /dev/null
@@ -0,0 +1,31 @@
+<fieldset>
+  <legend>
+    <h1 class="cds--type-heading-03">NVMe over Fabrics (TCP)</h1>
+  <cd-help-text>Monitor and manage NVMe-over-TCP resources for high-performance block storage.</cd-help-text>
+  </legend>
+</fieldset>
+<section>
+  <cds-tabs type="contained"
+            followFocus="true"
+            isNavigation="true"
+            [cacheActive]="false">
+  <cds-tab
+    heading="Gateways"
+    i18n-heading
+    [active]="activeTab === Tabs.gateways"
+    (selected)="onSelected(Tabs.gateways)">
+  </cds-tab>
+  <cds-tab
+    heading="Subsystems"
+    i18n-heading
+    [active]="activeTab === Tabs.subsystems"
+    (selected)="onSelected(Tabs.subsystems)">
+  </cds-tab>
+  <cds-tab
+    heading="Namespaces"
+    i18n-heading
+    [active]="activeTab === Tabs.namespaces"
+    (selected)="onSelected(Tabs.namespaces)">
+  </cds-tab>
+</cds-tabs>
+</section>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.spec.ts
new file mode 100644 (file)
index 0000000..cdd1ea2
--- /dev/null
@@ -0,0 +1,81 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { TabsModule } from 'carbon-components-angular';
+
+import { NvmeofTabsComponent } from './nvmeof-tabs.component';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('NvmeofTabsComponent', () => {
+  let component: NvmeofTabsComponent;
+  let fixture: ComponentFixture<NvmeofTabsComponent>;
+  let router: Router;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [NvmeofTabsComponent],
+      imports: [RouterTestingModule, SharedModule, TabsModule]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(NvmeofTabsComponent);
+    component = fixture.componentInstance;
+    router = TestBed.inject(Router);
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should default activeTab to gateways', () => {
+    jest.spyOn(router, 'url', 'get').mockReturnValue('/block/nvmeof/gateways');
+    component.ngOnInit();
+    expect(component.activeTab).toBe(component.Tabs.gateways);
+  });
+
+  it('should set activeTab to subsystems when URL contains subsystems', () => {
+    jest.spyOn(router, 'url', 'get').mockReturnValue('/block/nvmeof/subsystems');
+    component.ngOnInit();
+    expect(component.activeTab).toBe(component.Tabs.subsystems);
+  });
+
+  it('should set activeTab to namespaces when URL contains namespaces', () => {
+    jest.spyOn(router, 'url', 'get').mockReturnValue('/block/nvmeof/namespaces');
+    component.ngOnInit();
+    expect(component.activeTab).toBe(component.Tabs.namespaces);
+  });
+
+  it('should fallback to gateways when URL does not match any tab', () => {
+    jest.spyOn(router, 'url', 'get').mockReturnValue('/block/nvmeof/unknown');
+    component.ngOnInit();
+    expect(component.activeTab).toBe(component.Tabs.gateways);
+  });
+
+  it('should navigate to correct path on tab selection', () => {
+    spyOn(router, 'navigate');
+    component.onSelected(component.Tabs.subsystems);
+    expect(component.selectedTab).toBe(component.Tabs.subsystems);
+    expect(router.navigate).toHaveBeenCalledWith(['block/nvmeof/subsystems']);
+  });
+
+  it('should navigate to gateways on selecting gateways tab', () => {
+    spyOn(router, 'navigate');
+    component.onSelected(component.Tabs.gateways);
+    expect(component.selectedTab).toBe(component.Tabs.gateways);
+    expect(router.navigate).toHaveBeenCalledWith(['block/nvmeof/gateways']);
+  });
+
+  it('should navigate to namespaces on selecting namespaces tab', () => {
+    spyOn(router, 'navigate');
+    component.onSelected(component.Tabs.namespaces);
+    expect(component.selectedTab).toBe(component.Tabs.namespaces);
+    expect(router.navigate).toHaveBeenCalledWith(['block/nvmeof/namespaces']);
+  });
+
+  it('should expose TABS enum via Tabs getter', () => {
+    const tabs = component.Tabs;
+    expect(tabs.gateways).toBe('gateways');
+    expect(tabs.subsystems).toBe('subsystems');
+    expect(tabs.namespaces).toBe('namespaces');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.ts
new file mode 100644 (file)
index 0000000..8b74346
--- /dev/null
@@ -0,0 +1,37 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+
+const NVMEOF_PATH = 'block/nvmeof';
+
+enum TABS {
+  gateways = 'gateways',
+  subsystems = 'subsystems',
+  namespaces = 'namespaces'
+}
+
+@Component({
+  selector: 'cd-nvmeof-tabs',
+  templateUrl: './nvmeof-tabs.component.html',
+  styleUrls: ['./nvmeof-tabs.component.scss'],
+  standalone: false
+})
+export class NvmeofTabsComponent implements OnInit {
+  selectedTab: TABS;
+  activeTab: TABS = TABS.gateways;
+
+  constructor(private router: Router) {}
+
+  ngOnInit(): void {
+    const currentPath = this.router.url;
+    this.activeTab = Object.values(TABS).find((tab) => currentPath.includes(tab)) || TABS.gateways;
+  }
+
+  onSelected(tab: TABS) {
+    this.selectedTab = tab;
+    this.router.navigate([`${NVMEOF_PATH}/${tab}`]);
+  }
+
+  public get Tabs(): typeof TABS {
+    return TABS;
+  }
+}