From: pujaoshahu Date: Wed, 4 Mar 2026 08:32:54 +0000 (+0530) Subject: mgr/dashboard: Breadcrumb should allow going back to subsystem tab X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=419b0e8fbaa23a2ab8e61496c64c82f8108d24bb;p=ceph.git mgr/dashboard: Breadcrumb should allow going back to subsystem tab Fixes: https://tracker.ceph.com/issues/75288 Signed-off-by: pujaoshahu (cherry picked from commit 69a7c6bf151a1e1d256b15c4dd1e5c590145005e) --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts index cefe4268930d..56e694a26bbd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts @@ -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 } } ] } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.html index c3aaa0d4a4fc..7f48a42cbf63 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.html @@ -1,3 +1,5 @@ + + { 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]); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html index 942051ef37ab..9d8587bebe9e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html @@ -11,12 +11,6 @@ [columnNumbers]="{sm: 4, md: 8}">
-

{{ action | titlecase }} {{ resource }}

- - - A logical group of gateways that hosts will connect to. - -
{ this.router.navigate([this.pageURL], { - queryParams: { group: this.group, tab: 'namespace' } + queryParams: { group: this.group } }); } }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.html index dde00bf70112..ef76b9223fa4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.html @@ -1,3 +1,5 @@ + +
+ + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts index 5f5cbeccef48..e4a74bd917fc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts @@ -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-subsystems/nvmeof-subsystems.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.html index f61913a2a07d..fee4d1c0770b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.html @@ -1,3 +1,5 @@ + +
+ +

NVMe over Fabrics (TCP)

+ Monitor and manage NVMe-over-TCP resources for high-performance block storage. +
+ +
+ + + + + + + + +
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 index 000000000000..e69de29bb2d1 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 index 000000000000..cdd1ea2e9cfd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.spec.ts @@ -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; + 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 index 000000000000..8b74346db920 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.ts @@ -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; + } +}