From: pujaoshahu Date: Mon, 23 Feb 2026 18:31:30 +0000 (+0530) Subject: mgr/dashboard: Remove tabs under subsystem X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=9ce443cb135ea6f9cc7d8e4fcd5ffb4e13ef8ffb;p=ceph.git mgr/dashboard: Remove tabs under subsystem Fixes: https://tracker.ceph.com/issues/74904 Signed-off-by: pujaoshahu --- 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 edb4a1ec7d3..52677a13ebf 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 @@ -96,8 +96,10 @@ import { NvmeGatewayViewBreadcrumbResolver } from './nvme-gateway-view/nvme-gate import { NvmeofGatewayNodeMode } from '~/app/shared/enum/nvmeof.enum'; import { NvmeofGatewayNodeAddModalComponent } from './nvmeof-gateway-node/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component'; import { NvmeofSubsystemNamespacesListComponent } from './nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component'; +import { NvmeofSubsystemOverviewComponent } from './nvmeof-subsystem-overview/nvmeof-subsystem-overview.component'; 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'; @NgModule({ imports: [ @@ -180,7 +182,9 @@ import { NvmeSubsystemViewComponent } from './nvme-subsystem-view/nvme-subsystem NvmeofGatewayNodeAddModalComponent, NvmeofNamespaceExpandModalComponent, NvmeSubsystemViewComponent, - NvmeofEditHostKeyModalComponent + NvmeofEditHostKeyModalComponent, + NvmeofSubsystemOverviewComponent, + NvmeofSubsystemPerformanceComponent ], exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent] @@ -340,7 +344,6 @@ const routes: Routes = [ { path: 'gateways', component: NvmeofGatewayComponent, - data: { breadcrumbs: 'Gateways' }, children: [ { path: `${URLVerbs.EDIT}/:subsystem_nqn/namespace/:nsid`, @@ -420,7 +423,11 @@ const routes: Routes = [ component: NvmeSubsystemViewComponent, data: { breadcrumbs: NvmeSubsystemViewBreadcrumbResolver }, children: [ - { path: '', redirectTo: 'namespaces', pathMatch: 'full' }, + { path: '', redirectTo: 'overview', pathMatch: 'full' }, + { + path: 'overview', + component: NvmeofSubsystemOverviewComponent + }, { path: 'hosts', component: NvmeofInitiatorsListComponent @@ -432,6 +439,10 @@ const routes: Routes = [ { path: 'listeners', component: NvmeofListenersListComponent + }, + { + path: 'performance', + component: NvmeofSubsystemPerformanceComponent } ] } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.ts index 5941593bfe3..97ef80e06e7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.ts @@ -30,6 +30,11 @@ export class NvmeSubsystemViewComponent implements OnInit { private buildSidebarItems() { const extras = { queryParams: { group: this.groupName } }; this.sidebarItems = [ + { + label: $localize`Overview`, + route: [this.basePath, this.subsystemNQN, 'overview'], + routeExtras: extras + }, { label: $localize`Initiators`, route: [this.basePath, this.subsystemNQN, 'hosts'], @@ -44,6 +49,11 @@ export class NvmeSubsystemViewComponent implements OnInit { label: $localize`Listeners`, route: [this.basePath, this.subsystemNQN, 'listeners'], routeExtras: extras + }, + { + label: $localize`Performance`, + route: [this.basePath, this.subsystemNQN, 'performance'], + routeExtras: extras } ]; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html index 9da8c7c14ea..d586dcc6730 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html @@ -11,7 +11,7 @@ [maxLimit]="25" identifier="hostname" forceIdentifier="true" - [autoReload]="false" + [autoReload]="true" (updateSelection)="updateSelection($event)" emptyStateTitle="No nodes available" i18n-emptyStateTitle diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.html index 84db49150d8..4e2ebf2315e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.html @@ -2,6 +2,7 @@ { max_namespaces: 256, namespace_count: 0, subtype: 'NVMe', - namespaces: [] + namespaces: [], + has_dhchap_key: true } as NvmeofSubsystem, { nqn: 'nqn.2014-08.org.nvmexpress:uuid:2222', @@ -41,7 +42,8 @@ describe('NvmeofGatewaySubsystemComponent', () => { max_namespaces: 256, namespace_count: 0, subtype: 'NVMe', - namespaces: [] + namespaces: [], + has_dhchap_key: true } as NvmeofSubsystem ]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html index a17657ae49e..3af5b947f0a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html @@ -10,7 +10,7 @@ isNavigation="true" [cacheActive]="false"> { let component: NvmeofGatewayComponent; let fixture: ComponentFixture; + let breadcrumbService: BreadcrumbService; + let router: Router; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -25,6 +28,7 @@ describe('NvmeofGatewayComponent', () => { TabsModule ], providers: [ + BreadcrumbService, { provide: ActivatedRoute, useValue: { @@ -36,10 +40,36 @@ describe('NvmeofGatewayComponent', () => { fixture = TestBed.createComponent(NvmeofGatewayComponent); component = fixture.componentInstance; + breadcrumbService = TestBed.inject(BreadcrumbService); + router = TestBed.inject(Router); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set tab crumb on init', () => { + spyOn(breadcrumbService, 'setTabCrumb'); + component.ngOnInit(); + expect(breadcrumbService.setTabCrumb).toHaveBeenCalledWith('Gateways'); + }); + + it('should update tab crumb on tab switch', () => { + spyOn(router, 'navigate'); + spyOn(breadcrumbService, 'setTabCrumb'); + component.onSelected(component.Tabs.subsystem); + expect(router.navigate).toHaveBeenCalledWith([], { + relativeTo: TestBed.inject(ActivatedRoute), + queryParams: { tab: component.Tabs.subsystem }, + queryParamsHandling: 'merge' + }); + expect(breadcrumbService.setTabCrumb).toHaveBeenCalledWith('Subsystem'); + }); + + it('should clear tab crumb on destroy', () => { + spyOn(breadcrumbService, 'clearTabCrumb'); + component.ngOnDestroy(); + expect(breadcrumbService.clearTabCrumb).toHaveBeenCalled(); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts index 7f2eebf1d12..2927fb1615c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts @@ -1,10 +1,13 @@ -import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { Subject } from 'rxjs'; +import { filter, takeUntil } from 'rxjs/operators'; import _ from 'lodash'; import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { BreadcrumbService } from '~/app/shared/services/breadcrumb.service'; enum TABS { gateways = 'gateways', @@ -12,33 +15,68 @@ enum TABS { namespace = 'namespace' } +const TAB_LABELS: Record = { + [TABS.gateways]: $localize`Gateways`, + [TABS.subsystem]: $localize`Subsystem`, + [TABS.namespace]: $localize`Namespace` +}; + @Component({ selector: 'cd-nvmeof-gateway', templateUrl: './nvmeof-gateway.component.html', styleUrls: ['./nvmeof-gateway.component.scss'], standalone: false }) -export class NvmeofGatewayComponent implements OnInit { +export class NvmeofGatewayComponent implements OnInit, OnDestroy { selectedTab: TABS; activeTab: TABS = TABS.gateways; + private readonly destroy$ = new Subject(); @ViewChild('statusTpl', { static: true }) statusTpl: TemplateRef; selection = new CdTableSelection(); - constructor(public actionLabels: ActionLabelsI18n, private route: ActivatedRoute) {} + constructor( + public actionLabels: ActionLabelsI18n, + private route: ActivatedRoute, + private router: Router, + private breadcrumbService: BreadcrumbService + ) {} ngOnInit() { - this.route.queryParams.subscribe((params) => { + this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => { if (params['tab'] && Object.values(TABS).includes(params['tab'])) { this.activeTab = params['tab'] as TABS; } + this.breadcrumbService.setTabCrumb(TAB_LABELS[this.activeTab]); }); + + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + takeUntil(this.destroy$) + ) + .subscribe(() => { + // Run after NavigationEnd handlers so tab crumb is not cleared by global breadcrumb reset. + setTimeout(() => this.breadcrumbService.setTabCrumb(TAB_LABELS[this.activeTab])); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + this.breadcrumbService.clearTabCrumb(); } onSelected(tab: TABS) { this.selectedTab = tab; this.activeTab = tab; + this.router.navigate([], { + relativeTo: this.route, + queryParams: { tab }, + queryParamsHandling: 'merge' + }); + this.breadcrumbService.setTabCrumb(TAB_LABELS[tab]); } public get Tabs(): typeof TABS { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.html index 97a520db0c6..c535005fe2b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.html @@ -26,6 +26,7 @@ (fetchData)="listInitiators()" [columns]="initiatorColumns" selectionType="multiClick" + [autoReload]="false" (updateSelection)="updateSelection($event)">
{ component.subsystemNQN = 'nqn.2016-06.io.spdk:cnode1'; component.group = 'group1'; component.ngOnInit(); - fixture.detectChanges(); }); it('should create', () => { @@ -82,7 +81,7 @@ describe('NvmeofInitiatorsListComponent', () => { })); it('should update authStatus when initiator has dhchap_key', fakeAsync(() => { - const initiatorsWithKey = [{ nqn: 'nqn1', dhchap_key: 'key1' }]; + const initiatorsWithKey = [{ nqn: 'nqn1', use_dhchap: 'key1' }]; spyOn(TestBed.inject(NvmeofService), 'getInitiators').and.returnValue(of(initiatorsWithKey)); component.listInitiators(); tick(); @@ -90,7 +89,8 @@ describe('NvmeofInitiatorsListComponent', () => { })); it('should update authStatus when subsystem has psk', fakeAsync(() => { - const subsystemWithPsk = { ...mockSubsystem, psk: 'psk1' }; + const subsystemWithPsk = { ...mockSubsystem, has_dhchap_key: true }; + component.initiators = [{ nqn: 'nqn1', use_dhchap: 'key1' }]; spyOn(TestBed.inject(NvmeofService), 'getSubsystem').and.returnValue(of(subsystemWithPsk)); component.getSubsystem(); tick(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html index 2b605c2cce5..d806ec54025 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html @@ -11,6 +11,7 @@ (fetchData)="listListeners()" [columns]="listenerColumns" identifier="id" + [autoReload]="true" forceIdentifier="true" selectionType="single" (updateSelection)="updateSelection($event)"> 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 a600eadc582..f3e43fe6d60 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 @@ -25,6 +25,7 @@ ({ + ...g, + selected: g.content === this.group + })); + } this.gwGroupsEmpty = false; this.gwGroupPlaceholder = DEFAULT_PLACEHOLDER; - } else if (!this.gwGroups.length) { + } else { this.gwGroupsEmpty = true; this.gwGroupPlaceholder = $localize`No groups available`; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.html index 8913b850cf0..5a8a035ed7f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.html @@ -1,6 +1,7 @@ +

Subsystem details

+ +
+
+ Serial number + {{ subsystem.serial_number }} +
+
+ Model Number + {{ subsystem.model_number }} +
+
+ Gateway group + {{ subsystem.gw_group || groupName }} +
+ +
+ Subsystem Type + {{ subsystem.subtype }} +
+
+ HA Enabled + {{ subsystem.enable_ha ? 'Yes' : 'No' }} +
+
+ Hosts allowed + {{ subsystem.allow_any_host ? 'Any host' : 'Restricted' }} +
+ +
+ Maximum Controller Identifier + {{ subsystem.max_cntlid }} +
+
+ Minimum Controller Identifier + {{ subsystem.min_cntlid }} +
+
+ +
+ Namespaces + {{ subsystem.namespace_count }} +
+
+ Maximum allowed namespaces + {{ subsystem.max_namespaces }} +
+
+
+ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.scss new file mode 100644 index 00000000000..6270f1ecb84 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.scss @@ -0,0 +1,18 @@ +@use '@carbon/layout'; + +.tile-title { + margin-bottom: layout.$spacing-06; +} + +.details-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + row-gap: layout.$spacing-06; + column-gap: layout.$spacing-07; +} + +.detail-item { + display: flex; + flex-direction: column; + gap: layout.$spacing-02; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.spec.ts new file mode 100644 index 00000000000..ad7f77d4bb1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.spec.ts @@ -0,0 +1,181 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; + +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { GridModule, TilesModule } from 'carbon-components-angular'; + +import { NvmeofSubsystemOverviewComponent } from './nvmeof-subsystem-overview.component'; +import { NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { SharedModule } from '~/app/shared/shared.module'; + +describe('NvmeofSubsystemOverviewComponent', () => { + let component: NvmeofSubsystemOverviewComponent; + let fixture: ComponentFixture; + let nvmeofService: NvmeofService; + + const mockSubsystem = { + nqn: 'nqn.2016-06.io.spdk:cnode1', + serial_number: 'Ceph30487186726692', + model_number: 'Ceph bdev Controller', + min_cntlid: 1, + max_cntlid: 2040, + subtype: 'NVMe', + namespace_count: 3, + max_namespaces: 256, + enable_ha: true, + allow_any_host: true, + gw_group: 'gateway-prod', + psk: 'some-key' + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [NvmeofSubsystemOverviewComponent], + imports: [ + HttpClientTestingModule, + RouterTestingModule, + SharedModule, + NgbTooltipModule, + TilesModule, + GridModule + ], + providers: [ + { + provide: ActivatedRoute, + useValue: { + parent: { + params: of({ subsystem_nqn: 'nqn.2016-06.io.spdk:cnode1' }) + }, + queryParams: of({ group: 'group1' }) + } + }, + { + provide: NvmeofService, + useValue: { + getSubsystem: jest.fn().mockReturnValue(of(mockSubsystem)) + } + } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NvmeofSubsystemOverviewComponent); + component = fixture.componentInstance; + nvmeofService = TestBed.inject(NvmeofService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should fetch subsystem on init', fakeAsync(() => { + component.ngOnInit(); + tick(); + expect(nvmeofService.getSubsystem).toHaveBeenCalledWith('nqn.2016-06.io.spdk:cnode1', 'group1'); + })); + + it('should store subsystem data', fakeAsync(() => { + component.ngOnInit(); + tick(); + expect(component.subsystem).toEqual(mockSubsystem); + expect(component.subsystem.serial_number).toBe('Ceph30487186726692'); + expect(component.subsystem.model_number).toBe('Ceph bdev Controller'); + expect(component.subsystem.max_cntlid).toBe(2040); + expect(component.subsystem.min_cntlid).toBe(1); + expect(component.subsystem.namespace_count).toBe(3); + expect(component.subsystem.max_namespaces).toBe(256); + expect(component.subsystem.gw_group).toBe('gateway-prod'); + })); + + it('should not fetch when subsystemNQN is missing', fakeAsync(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + declarations: [NvmeofSubsystemOverviewComponent], + imports: [ + HttpClientTestingModule, + RouterTestingModule, + SharedModule, + NgbTooltipModule, + TilesModule, + GridModule + ], + providers: [ + { + provide: ActivatedRoute, + useValue: { + parent: { + params: of({}) + }, + queryParams: of({ group: 'group1' }) + } + }, + { + provide: NvmeofService, + useValue: { + getSubsystem: jest.fn().mockReturnValue(of(mockSubsystem)) + } + } + ] + }).compileComponents(); + + const newFixture = TestBed.createComponent(NvmeofSubsystemOverviewComponent); + const newComponent = newFixture.componentInstance; + const newService = TestBed.inject(NvmeofService); + newFixture.detectChanges(); + tick(); + expect(newService.getSubsystem).not.toHaveBeenCalled(); + expect(newComponent.subsystem).toBeUndefined(); + })); + + it('should render detail labels in the template', fakeAsync(() => { + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const labels = compiled.querySelectorAll('.cds--type-label-01'); + const labelTexts = Array.from(labels).map((el: HTMLElement) => el.textContent.trim()); + expect(labelTexts).toContain('Serial number'); + expect(labelTexts).toContain('Model Number'); + expect(labelTexts).toContain('Gateway group'); + expect(labelTexts).toContain('Maximum Controller Identifier'); + expect(labelTexts).toContain('Minimum Controller Identifier'); + expect(labelTexts).toContain('Namespaces'); + expect(labelTexts).toContain('Maximum allowed namespaces'); + })); + + it('should display subsystem type from subsystem data', fakeAsync(() => { + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const values = fixture.nativeElement.querySelectorAll('.cds--type-body-compact-01'); + const valueTexts = Array.from(values).map((el: HTMLElement) => el.textContent.trim()); + expect(valueTexts).toContain('NVMe'); + })); + + it('should display hosts allowed from subsystem data', fakeAsync(() => { + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const values = fixture.nativeElement.querySelectorAll('.cds--type-body-compact-01'); + const valueTexts = Array.from(values).map((el: HTMLElement) => el.textContent.trim()); + expect(valueTexts).toContain('Any host'); + })); + + it('should display HA status from subsystem data', fakeAsync(() => { + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const values = fixture.nativeElement.querySelectorAll('.cds--type-body-compact-01'); + const valueTexts = Array.from(values).map((el: HTMLElement) => el.textContent.trim()); + expect(valueTexts).toContain('Yes'); + })); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.ts new file mode 100644 index 00000000000..a414dcf6c96 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.ts @@ -0,0 +1,43 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { NvmeofSubsystem } from '~/app/shared/models/nvmeof'; + +@Component({ + selector: 'cd-nvmeof-subsystem-overview', + templateUrl: './nvmeof-subsystem-overview.component.html', + styleUrls: ['./nvmeof-subsystem-overview.component.scss'], + standalone: false +}) +export class NvmeofSubsystemOverviewComponent implements OnInit { + subsystemNQN: string; + groupName: string; + subsystem: NvmeofSubsystem; + + constructor(private route: ActivatedRoute, private nvmeofService: NvmeofService) {} + + ngOnInit() { + this.route.parent?.params.subscribe((params) => { + this.subsystemNQN = params['subsystem_nqn']; + this.fetchIfReady(); + }); + this.route.queryParams.subscribe((qp) => { + this.groupName = qp['group']; + this.fetchIfReady(); + }); + } + + private fetchIfReady() { + if (this.subsystemNQN && this.groupName) { + this.fetchSubsystem(); + } + } + + fetchSubsystem() { + this.nvmeofService + .getSubsystem(this.subsystemNQN, this.groupName) + .subscribe((subsystem: NvmeofSubsystem) => { + this.subsystem = subsystem; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.html new file mode 100644 index 00000000000..1a40ac4ed38 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.html @@ -0,0 +1,13 @@ + + + + + Grafana permissions are required to view performance details. + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.spec.ts new file mode 100644 index 00000000000..a05e91c30b7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.spec.ts @@ -0,0 +1,62 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; + +import { NvmeofSubsystemPerformanceComponent } from './nvmeof-subsystem-performance.component'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { Permissions } from '~/app/shared/models/permissions'; +import { SharedModule } from '~/app/shared/shared.module'; + +describe('NvmeofSubsystemPerformanceComponent', () => { + let component: NvmeofSubsystemPerformanceComponent; + let fixture: ComponentFixture; + + const mockPermissions = new Permissions({ grafana: ['read'] }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [NvmeofSubsystemPerformanceComponent], + imports: [HttpClientTestingModule, RouterTestingModule, SharedModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + parent: { + params: of({ subsystem_nqn: 'nqn.2016-06.io.spdk:cnode1' }) + }, + queryParams: of({ group: 'group1' }) + } + }, + { + provide: AuthStorageService, + useValue: { + getPermissions: jest.fn().mockReturnValue(mockPermissions) + } + } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NvmeofSubsystemPerformanceComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set subsystemNQN and groupName from route params', fakeAsync(() => { + component.ngOnInit(); + tick(); + expect(component.subsystemNQN).toBe('nqn.2016-06.io.spdk:cnode1'); + expect(component.groupName).toBe('group1'); + })); + + it('should have grafana read permission', () => { + expect(component.permissions.grafana.read).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.ts new file mode 100644 index 00000000000..e54087c7bcb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.ts @@ -0,0 +1,29 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Permissions } from '~/app/shared/models/permissions'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; + +@Component({ + selector: 'cd-nvmeof-subsystem-performance', + templateUrl: './nvmeof-subsystem-performance.component.html', + styleUrls: ['./nvmeof-subsystem-performance.component.scss'], + standalone: false +}) +export class NvmeofSubsystemPerformanceComponent implements OnInit { + subsystemNQN: string; + groupName: string; + permissions: Permissions; + + constructor(private route: ActivatedRoute, private authStorageService: AuthStorageService) { + this.permissions = this.authStorageService.getPermissions(); + } + + ngOnInit() { + this.route.parent?.params.subscribe((params) => { + this.subsystemNQN = params['subsystem_nqn']; + }); + this.route.queryParams.subscribe((qp) => { + this.groupName = qp['group']; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.html index ca9f7d7ef19..14a75e15bcc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.html @@ -11,35 +11,6 @@ - - Listeners - - - - - - - Namespaces - - - - - - - Initiators - - - - - { subtype: 'NVMe', nqn: 'nqn.2001-07.com.ceph:1720603703820', namespace_count: 1, + has_dhchap_key: false, max_namespaces: DEFAULT_MAX_NAMESPACE_PER_SUBSYSTEM }; component.permissions = new Permissions({ 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 20c797391b2..f61913a2a07 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 @@ -26,8 +26,6 @@ [columns]="subsystemsColumns" columnMode="flex" selectionType="single" - [hasDetails]="true" - (setExpandedRow)="setExpandedRow($event)" (updateSelection)="updateSelection($event)" (fetchData)="fetchData()" emptyStateTitle="No subsystems created" diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts index 67ec2448363..91bdf4e9bf7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts @@ -119,11 +119,6 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit name: $localize`Authentication`, prop: 'authentication', cellTemplate: this.authenticationTpl - }, - { - name: $localize`Traffic encryption`, - prop: 'encryption', - cellTemplate: this.encryptionTpl } ]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts index 4e6a4338a62..0b4cd23a09b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts @@ -31,6 +31,7 @@ import { distinct, filter, first, mergeMap, toArray } from 'rxjs/operators'; import { AppConstants } from '~/app/shared/constants/app.constants'; import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs'; +import { BreadcrumbService } from '~/app/shared/services/breadcrumb.service'; @Component({ selector: 'cd-breadcrumbs', @@ -49,9 +50,17 @@ export class BreadcrumbsComponent implements OnDestroy { */ finished = false; subscription: Subscription; + private tabCrumbSubscription: Subscription; private defaultResolver = new BreadcrumbsResolver(); - - constructor(private router: Router, private injector: Injector, private titleService: Title) { + private baseCrumbs: IBreadcrumb[] = []; + private currentTabCrumb: IBreadcrumb = null; + + constructor( + private router: Router, + private injector: Injector, + private titleService: Title, + private breadcrumbService: BreadcrumbService + ) { this.subscription = this.router.events .pipe(filter((x) => x instanceof NavigationStart)) .subscribe(() => { @@ -61,6 +70,7 @@ export class BreadcrumbsComponent implements OnDestroy { this.subscription = this.router.events .pipe(filter((x) => x instanceof NavigationEnd)) .subscribe(() => { + this.breadcrumbService.clearTabCrumb(); const currentRoot = router.routerState.snapshot.root; this._resolveCrumbs(currentRoot) @@ -75,15 +85,28 @@ export class BreadcrumbsComponent implements OnDestroy { ) .subscribe((x) => { this.finished = true; - this.crumbs = x; + this.baseCrumbs = x; + this.crumbs = this.currentTabCrumb ? [...x, this.currentTabCrumb] : [...x]; const title = this.getTitleFromCrumbs(this.crumbs); this.titleService.setTitle(title); }); }); + + this.tabCrumbSubscription = this.breadcrumbService.tabCrumb$.subscribe((tabCrumb) => { + this.currentTabCrumb = tabCrumb; + if (tabCrumb) { + this.crumbs = [...this.baseCrumbs, tabCrumb]; + } else { + this.crumbs = [...this.baseCrumbs]; + } + const title = this.getTitleFromCrumbs(this.crumbs); + this.titleService.setTitle(title); + }); } ngOnDestroy(): void { this.subscription.unsubscribe(); + this.tabCrumbSubscription.unsubscribe(); } private _resolveCrumbs(route: ActivatedRouteSnapshot): Observable { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts index c7cfec1209e..26d17b81d6b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts @@ -23,7 +23,7 @@ export interface NvmeofSubsystem { enable_ha?: boolean; gw_group?: string; initiator_count?: number; - psk?: string; + has_dhchap_key: boolean; } export interface NvmeofSubsystemData extends NvmeofSubsystem { @@ -33,7 +33,7 @@ export interface NvmeofSubsystemData extends NvmeofSubsystem { export interface NvmeofSubsystemInitiator { nqn: string; - dhchap_key?: string; + use_dhchap?: string; } export interface NvmeofListener { @@ -96,10 +96,6 @@ export function getSubsystemAuthStatus( const UNIDIRECTIONAL = 'Unidirectional'; const BIDIRECTIONAL = 'Bi-directional'; - if (subsystem.psk) { - return BIDIRECTIONAL; - } - let hostsList: NvmeofSubsystemInitiator[] = []; if (_initiators && 'hosts' in _initiators && Array.isArray(_initiators.hosts)) { hostsList = _initiators.hosts; @@ -107,12 +103,19 @@ export function getSubsystemAuthStatus( hostsList = _initiators as NvmeofSubsystemInitiator[]; } - const hasDhchapKey = hostsList.some((host) => !!host.dhchap_key); - if (hasDhchapKey) { - return UNIDIRECTIONAL; + let auth = NO_AUTH; + + const hostHasDhchapKey = hostsList.some((host) => !!host.use_dhchap); + + if (hostHasDhchapKey) { + auth = UNIDIRECTIONAL; + } + + if (subsystem.has_dhchap_key && hostHasDhchapKey) { + auth = BIDIRECTIONAL; } - return NO_AUTH; + return auth; } // Form control names for NvmeofNamespacesFormComponent diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/breadcrumb.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/breadcrumb.service.ts new file mode 100644 index 00000000000..8ef1a89982f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/breadcrumb.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +import { IBreadcrumb } from '~/app/shared/models/breadcrumbs'; + +@Injectable({ + providedIn: 'root' +}) +export class BreadcrumbService { + private tabCrumbSubject = new BehaviorSubject(null); + tabCrumb$ = this.tabCrumbSubject.asObservable(); + + setTabCrumb(text: string, path: string = null): void { + this.tabCrumbSubject.next({ text, path }); + } + + clearTabCrumb(): void { + this.tabCrumbSubject.next(null); + } +}