From: pujaoshahu Date: Mon, 2 Feb 2026 08:46:20 +0000 (+0530) Subject: mgr/dashboard: NVMe – Fix host,listeners namespace list display on Subsystem resource... X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=50177830211892d0720f724909f74c84bd72be30;p=ceph.git mgr/dashboard: NVMe – Fix host,listeners namespace list display on Subsystem resource page Fixes: https://tracker.ceph.com/issues/74697 Signed-off-by: pujaoshahu Conflicts: src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts 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 ed0583ef9b4..92ed9d76872 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 @@ -92,6 +92,9 @@ import { NvmeGatewayViewComponent } from './nvme-gateway-view/nvme-gateway-view. import { NvmeGatewayViewBreadcrumbResolver } from './nvme-gateway-view/nvme-gateway-view-breadcrumb.resolver'; 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 { NvmeSubsystemViewBreadcrumbResolver } from './nvme-subsystem-view/nvme-subsystem-view-breadcrumb.resolver'; +import { NvmeSubsystemViewComponent } from './nvme-subsystem-view/nvme-subsystem-view.component'; @NgModule({ imports: [ @@ -122,11 +125,10 @@ import { NvmeofGatewayNodeAddModalComponent } from './nvmeof-gateway-node/nvmeof TagModule, GridModule, LayerModule, - LayoutModule, ContainedListModule, SideNavModule, - ThemeModule, - LayoutModule + LayoutModule, + ThemeModule ], declarations: [ RbdListComponent, @@ -161,6 +163,7 @@ import { NvmeofGatewayNodeAddModalComponent } from './nvmeof-gateway-node/nvmeof NvmeofListenersFormComponent, NvmeofListenersListComponent, NvmeofNamespacesListComponent, + NvmeofSubsystemNamespacesListComponent, NvmeofNamespacesFormComponent, NvmeofInitiatorsListComponent, NvmeofInitiatorsFormComponent, @@ -171,7 +174,8 @@ import { NvmeofGatewayNodeAddModalComponent } from './nvmeof-gateway-node/nvmeof NvmeofSubsystemsStepThreeComponent, NvmeGatewayViewComponent, NvmeofGatewaySubsystemComponent, - NvmeofGatewayNodeAddModalComponent + NvmeofGatewayNodeAddModalComponent, + NvmeSubsystemViewComponent ], exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent] @@ -388,6 +392,26 @@ const routes: Routes = [ outlet: 'modal' } ] + }, + { + path: `subsystems/:subsystem_nqn`, + component: NvmeSubsystemViewComponent, + data: { breadcrumbs: NvmeSubsystemViewBreadcrumbResolver }, + children: [ + { path: '', redirectTo: 'namespaces', pathMatch: 'full' }, + { + path: 'hosts', + component: NvmeofInitiatorsListComponent + }, + { + path: 'namespaces', + component: NvmeofSubsystemNamespacesListComponent + }, + { + path: 'listeners', + component: NvmeofListenersListComponent + } + ] } ] } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view-breadcrumb.resolver.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view-breadcrumb.resolver.spec.ts new file mode 100644 index 00000000000..6cf22aadd95 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view-breadcrumb.resolver.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { NvmeSubsystemViewBreadcrumbResolver } from './nvme-subsystem-view-breadcrumb.resolver'; + +describe('NvmeSubsystemViewBreadcrumbResolver', () => { + let resolver: NvmeSubsystemViewBreadcrumbResolver; + + beforeEach(() => { + TestBed.configureTestingModule({}); + resolver = TestBed.inject(NvmeSubsystemViewBreadcrumbResolver); + }); + + it('should be created', () => { + expect(resolver).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view-breadcrumb.resolver.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view-breadcrumb.resolver.ts new file mode 100644 index 00000000000..804a2003417 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view-breadcrumb.resolver.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs'; + +@Injectable({ + providedIn: 'root' +}) +export class NvmeSubsystemViewBreadcrumbResolver extends BreadcrumbsResolver { + resolve(route: ActivatedRouteSnapshot): IBreadcrumb[] { + const subsystemNQN = route.parent?.params?.subsystem_nqn || route.params?.subsystem_nqn; + return [{ text: decodeURIComponent(subsystemNQN || ''), path: null }]; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.html new file mode 100644 index 00000000000..7b63beee3a6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.html @@ -0,0 +1,4 @@ + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.spec.ts new file mode 100644 index 00000000000..254b6884134 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.spec.ts @@ -0,0 +1,31 @@ +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { SideNavModule, ThemeModule } from 'carbon-components-angular'; + +import { NvmeSubsystemViewComponent } from './nvme-subsystem-view.component'; + +describe('NvmeSubsystemViewComponent', () => { + let component: NvmeSubsystemViewComponent; + let fixture: ComponentFixture; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [NvmeSubsystemViewComponent], + imports: [RouterTestingModule, SideNavModule, ThemeModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(NvmeSubsystemViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 00000000000..5941593bfe3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.ts @@ -0,0 +1,50 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { SidebarItem } from '~/app/shared/components/sidebar-layout/sidebar-layout.component'; + +@Component({ + selector: 'cd-nvme-subsystem-view', + templateUrl: './nvme-subsystem-view.component.html', + styleUrls: ['./nvme-subsystem-view.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class NvmeSubsystemViewComponent implements OnInit { + subsystemNQN: string; + groupName: string; + public readonly basePath = '/block/nvmeof/subsystems'; + sidebarItems: SidebarItem[] = []; + + constructor(private route: ActivatedRoute) {} + + ngOnInit() { + this.route.paramMap.subscribe((pm: ParamMap) => { + this.subsystemNQN = pm.get('subsystem_nqn') ?? ''; + }); + this.route.queryParams.subscribe((qp) => { + this.groupName = qp['group'] ?? ''; + this.buildSidebarItems(); + }); + } + + private buildSidebarItems() { + const extras = { queryParams: { group: this.groupName } }; + this.sidebarItems = [ + { + label: $localize`Initiators`, + route: [this.basePath, this.subsystemNQN, 'hosts'], + routeExtras: extras + }, + { + label: $localize`Namespaces`, + route: [this.basePath, this.subsystemNQN, 'namespaces'], + routeExtras: extras + }, + { + label: $localize`Listeners`, + route: [this.basePath, this.subsystemNQN, 'listeners'], + routeExtras: extras + } + ]; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.ts index e7f1c2f71eb..4b4ac02794f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.ts @@ -146,11 +146,6 @@ export class NvmeofGatewayGroupComponent implements OnInit { const subsystemsObservable = isRunning ? this.nvmeofService.listSubsystems(group.spec.group).pipe( catchError(() => { - this.notificationService.show( - NotificationType.error, - $localize`Unable to fetch Gateway group`, - $localize`Gateway group does not exist` - ); return of([]); }) ) @@ -173,11 +168,6 @@ export class NvmeofGatewayGroupComponent implements OnInit { ); }), catchError(() => { - this.notificationService.show( - NotificationType.error, - $localize`Unable to fetch Gateway group`, - $localize`Gateway group does not exist` - ); return of([]); }) ) @@ -185,7 +175,6 @@ export class NvmeofGatewayGroupComponent implements OnInit { ); this.checkNodesAvailability(); } - fetchData(): void { this.subject.next([]); this.checkNodesAvailability(); 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 37a344907f3..523c750caf1 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 @@ -13,18 +13,21 @@ heading="Gateway groups" [tabContent]="gateways_content" i18n-heading + [active]="activeTab === Tabs.gateways" (selected)="onSelected(Tabs.gateways)"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts index 893a3aefc2b..d1c9b9eeaf4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NvmeofGatewayComponent } from './nvmeof-gateway.component'; import { HttpClientModule } from '@angular/common/http'; +import { RouterTestingModule } from '@angular/router/testing'; import { SharedModule } from '~/app/shared/shared.module'; import { ComboBoxModule, GridModule, TabsModule } from 'carbon-components-angular'; @@ -13,7 +14,14 @@ describe('NvmeofGatewayComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [NvmeofGatewayComponent], - imports: [HttpClientModule, SharedModule, ComboBoxModule, GridModule, TabsModule], + imports: [ + HttpClientModule, + RouterTestingModule, + SharedModule, + ComboBoxModule, + GridModule, + TabsModule + ], providers: [] }).compileComponents(); 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 a0954e106aa..7f2eebf1d12 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,4 +1,5 @@ -import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import _ from 'lodash'; @@ -6,9 +7,9 @@ import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; enum TABS { - 'gateways', - 'subsystem', - 'namespace' + gateways = 'gateways', + subsystem = 'subsystem', + namespace = 'namespace' } @Component({ @@ -17,20 +18,30 @@ enum TABS { styleUrls: ['./nvmeof-gateway.component.scss'], standalone: false }) -export class NvmeofGatewayComponent { +export class NvmeofGatewayComponent implements OnInit { selectedTab: TABS; + activeTab: TABS = TABS.gateways; + + @ViewChild('statusTpl', { static: true }) + statusTpl: TemplateRef; + selection = new CdTableSelection(); + + constructor(public actionLabels: ActionLabelsI18n, private route: ActivatedRoute) {} + + ngOnInit() { + this.route.queryParams.subscribe((params) => { + if (params['tab'] && Object.values(TABS).includes(params['tab'])) { + this.activeTab = params['tab'] as TABS; + } + }); + } onSelected(tab: TABS) { this.selectedTab = tab; + this.activeTab = tab; } public get Tabs(): typeof TABS { return TABS; } - - @ViewChild('statusTpl', { static: true }) - statusTpl: TemplateRef; - selection = new CdTableSelection(); - - constructor(public actionLabels: ActionLabelsI18n) {} } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts index b0316782099..6f6b8f8896f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts @@ -5,6 +5,7 @@ import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder'; import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { CdValidators } from '~/app/shared/forms/cd-validators'; +import { Icons } from '~/app/shared/enum/icons.enum'; import { Permission } from '~/app/shared/models/permissions'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; @@ -12,8 +13,6 @@ import { FinishedTask } from '~/app/shared/models/finished-task'; import { ActivatedRoute, Router } from '@angular/router'; import { InitiatorRequest, NvmeofService } from '~/app/shared/api/nvmeof.service'; -import { Icons } from '~/app/shared/enum/icons.enum'; - @Component({ selector: 'cd-nvmeof-initiators-form', templateUrl: './nvmeof-initiators-form.component.html', 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 cb0139f7df2..e565c1ddb93 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 @@ -1,10 +1,29 @@ - - - An initiator (or host) is the client that connects to the NVMe-oF target to access NVMe storage. - The NVMe/TCP protocol allows initiators, to send NVMe-oF commands to storage devices, which are known as targets. - - - +
+
+ Host access: All hosts allowed +

+ Allowing all hosts grants access to every initiator on the network. Authentication is not supported in this mode, which may expose the subsystem to unauthorized access. +

+
+ + Edit host access + +
+ + + - - Any host allowed (*) - {{value}} + +
+ {{ authStatus !== authType.NO_AUTH ? 'Yes' : 'No' }} +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.spec.ts index f8d9c673632..5f51162e219 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.spec.ts @@ -14,19 +14,29 @@ import { NvmeofInitiatorsListComponent } from './nvmeof-initiators-list.componen const mockInitiators = [ { - nqn: '*' + nqn: '*', + dhchap_key: '' } ]; +const mockSubsystem = { + nqn: 'nqn.2016-06.io.spdk:cnode1', + serial_number: '12345', + psk: '' +}; + class MockNvmeOfService { getInitiators() { return of(mockInitiators); } + getSubsystem() { + return of(mockSubsystem); + } } class MockAuthStorageService { getPermissions() { - return { nvmeof: {} }; + return { nvmeof: { read: true, create: true, delete: true } }; } } @@ -52,6 +62,8 @@ describe('NvmeofInitiatorsListComponent', () => { fixture = TestBed.createComponent(NvmeofInitiatorsListComponent); component = fixture.componentInstance; + component.subsystemNQN = 'nqn.2016-06.io.spdk:cnode1'; + component.group = 'group1'; component.ngOnInit(); fixture.detectChanges(); }); @@ -60,9 +72,28 @@ describe('NvmeofInitiatorsListComponent', () => { expect(component).toBeTruthy(); }); - it('should retrieve initiators', fakeAsync(() => { + it('should retrieve initiators and subsystem', fakeAsync(() => { component.listInitiators(); + component.getSubsystem(); tick(); expect(component.initiators).toEqual(mockInitiators); + expect(component.subsystem).toEqual(mockSubsystem); + expect(component.authStatus).toBe('No authentication'); + })); + + it('should update authStatus when initiator has dhchap_key', fakeAsync(() => { + const initiatorsWithKey = [{ nqn: 'nqn1', dhchap_key: 'key1' }]; + spyOn(TestBed.inject(NvmeofService), 'getInitiators').and.returnValue(of(initiatorsWithKey)); + component.listInitiators(); + tick(); + expect(component.authStatus).toBe('Unidirectional'); + })); + + it('should update authStatus when subsystem has psk', fakeAsync(() => { + const subsystemWithPsk = { ...mockSubsystem, psk: 'psk1' }; + spyOn(TestBed.inject(NvmeofService), 'getSubsystem').and.returnValue(of(subsystemWithPsk)); + component.getSubsystem(); + tick(); + expect(component.authStatus).toBe('Bi-directional'); })); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts index 4cccb4e154f..4da1696fffe 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { NvmeofService } from '~/app/shared/api/nvmeof.service'; import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; @@ -7,8 +7,13 @@ import { Icons } from '~/app/shared/enum/icons.enum'; import { CdTableAction } from '~/app/shared/models/cd-table-action'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; import { FinishedTask } from '~/app/shared/models/finished-task'; -import { NvmeofSubsystemInitiator } from '~/app/shared/models/nvmeof'; +import { + NvmeofSubsystem, + NvmeofSubsystemInitiator, + getSubsystemAuthStatus +} from '~/app/shared/models/nvmeof'; import { Permission } from '~/app/shared/models/permissions'; +import { NvmeofSubsystemAuthType } from '~/app/shared/enum/nvmeof.enum'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; @@ -27,14 +32,17 @@ export class NvmeofInitiatorsListComponent implements OnInit { @Input() group: string; - @ViewChild('hostTpl', { static: true }) - hostTpl: TemplateRef; + @ViewChild('dhchapTpl', { static: true }) + dhchapTpl: TemplateRef; initiatorColumns: any; tableActions: CdTableAction[]; selection = new CdTableSelection(); permission: Permission; initiators: NvmeofSubsystemInitiator[] = []; + subsystem: NvmeofSubsystem; + authStatus: string; + authType = NvmeofSubsystemAuthType; constructor( public actionLabels: ActionLabelsI18n, @@ -42,17 +50,39 @@ export class NvmeofInitiatorsListComponent implements OnInit { private nvmeofService: NvmeofService, private modalService: ModalCdsService, private router: Router, - private taskWrapper: TaskWrapperService + private taskWrapper: TaskWrapperService, + private route: ActivatedRoute ) { this.permission = this.authStorageService.getPermissions().nvmeof; } ngOnInit() { + if (!this.subsystemNQN || !this.group) { + this.route.parent?.params.subscribe((params) => { + if (params['subsystem_nqn']) { + this.subsystemNQN = params['subsystem_nqn']; + } + this.fetchIfReady(); + }); + this.route.queryParams.subscribe((qp) => { + if (qp['group']) { + this.group = qp['group']; + } + this.fetchIfReady(); + }); + } else { + this.getSubsystem(); + } + this.initiatorColumns = [ { - name: $localize`Initiator`, - prop: 'nqn', - cellTemplate: this.hostTpl + name: $localize`Host NQN`, + prop: 'nqn' + }, + { + name: $localize`DHCHAP key`, + prop: 'dhchap_key', + cellTemplate: this.dhchapTpl } ]; this.tableActions = [ @@ -65,7 +95,8 @@ export class NvmeofInitiatorsListComponent implements OnInit { [BASE_URL, { outlets: { modal: [URLVerbs.ADD, this.subsystemNQN, 'initiator'] } }], { queryParams: { group: this.group } } ), - canBePrimary: (selection: CdTableSelection) => !selection.hasSelection + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection, + disable: () => this.hasAllHostsAllowed() }, { name: this.actionLabels.REMOVE, @@ -78,10 +109,28 @@ export class NvmeofInitiatorsListComponent implements OnInit { ]; } + private fetchIfReady() { + if (this.subsystemNQN && this.group) { + this.listInitiators(); + this.getSubsystem(); + } + } + getAllowAllHostIndex() { return this.selection.selected.findIndex((selected) => selected.nqn === '*'); } + hasAllHostsAllowed(): boolean { + return this.initiators.some((initiator) => initiator.nqn === '*'); + } + + editHostAccess() { + this.router.navigate( + [BASE_URL, { outlets: { modal: [URLVerbs.ADD, this.subsystemNQN, 'initiator'] } }], + { queryParams: { group: this.group } } + ); + } + updateSelection(selection: CdTableSelection) { this.selection = selection; } @@ -91,9 +140,23 @@ export class NvmeofInitiatorsListComponent implements OnInit { .getInitiators(this.subsystemNQN, this.group) .subscribe((initiators: NvmeofSubsystemInitiator[]) => { this.initiators = initiators; + this.updateAuthStatus(); }); } + getSubsystem() { + this.nvmeofService.getSubsystem(this.subsystemNQN, this.group).subscribe((subsystem: any) => { + this.subsystem = subsystem; + this.updateAuthStatus(); + }); + } + + updateAuthStatus() { + if (this.subsystem && this.initiators) { + this.authStatus = getSubsystemAuthStatus(this.subsystem, this.initiators); + } + } + getSelectedNQNs() { return this.selection.selected.map((selected) => selected.nqn); } 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 9c828321690..2b605c2cce5 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 @@ -1,8 +1,11 @@ - - - A listener defines the IP address and port on the gateway that is used to process NVMe/TCP admin and I/O commands to a subsystem. - - + + Currently, there are no listeners available in the NVMe subsystem. Please check your configuration or try again later. + + { + if (params['subsystem_nqn']) { + this.subsystemNQN = params['subsystem_nqn']; + } + if (this.subsystemNQN && this.group) { + this.listListeners(); + } + }); + this.route.queryParams.subscribe((qp) => { + if (qp['group']) { + this.group = qp['group']; + } + if (this.subsystemNQN && this.group) { + this.listListeners(); + } + }); + } + this.listenerColumns = [ { - name: $localize`Host`, + name: $localize`Name`, prop: 'host_name' }, { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-form/nvmeof-namespaces-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-form/nvmeof-namespaces-form.component.ts index 0c0c6b42466..8c837d3885d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-form/nvmeof-namespaces-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-form/nvmeof-namespaces-form.component.ts @@ -87,7 +87,8 @@ export class NvmeofNamespacesFormComponent implements OnInit { .getNamespace(this.subsystemNQN, this.nsid, this.group) .subscribe((res: NvmeofSubsystemNamespace) => { const convertedSize = this.dimlessBinaryPipe.transform(res.rbd_image_size).split(' '); - this.currentBytes = res.rbd_image_size; + this.currentBytes = + typeof res.rbd_image_size === 'string' ? Number(res.rbd_image_size) : res.rbd_image_size; this.nsForm.get('pool').setValue(res.rbd_pool_name); this.nsForm.get('unit').setValue(convertedSize[1]); this.nsForm.get('image_size').setValue(convertedSize[0]); 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 new file mode 100644 index 00000000000..8913b850cf0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.html @@ -0,0 +1,19 @@ + + +
+ + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.spec.ts new file mode 100644 index 00000000000..a21879a5d87 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.spec.ts @@ -0,0 +1,91 @@ +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 { NvmeofSubsystemNamespacesListComponent } from './nvmeof-subsystem-namespaces-list.component'; +import { NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { SharedModule } from '~/app/shared/shared.module'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; + +describe('NvmeofSubsystemNamespacesListComponent', () => { + let component: NvmeofSubsystemNamespacesListComponent; + let fixture: ComponentFixture; + let nvmeofService: NvmeofService; + + const mockNamespaces = [ + { + nsid: 1, + subsystem_nqn: 'nqn.2016-06.io.spdk:cnode1', + rbd_image_name: 'image1', + rbd_pool_name: 'pool1', + rbd_image_size: 1024, + block_size: 512, + rw_ios_per_second: 100 + }, + { + nsid: 2, + subsystem_nqn: 'nqn.2016-06.io.spdk:cnode2', // Different subsystem + rbd_image_name: 'image2', + rbd_pool_name: 'pool1', + rbd_image_size: 1024, + block_size: 512, + rw_ios_per_second: 100 + } + ]; + + class MockAuthStorageService { + getPermissions() { + return { nvmeof: {} }; + } + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [NvmeofSubsystemNamespacesListComponent], + imports: [HttpClientTestingModule, RouterTestingModule, SharedModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + parent: { + params: of({ subsystem_nqn: 'nqn.2016-06.io.spdk:cnode1', group: 'group1' }) + }, + queryParams: of({ group: 'group1' }) + } + }, + { + provide: NvmeofService, + useValue: { + listNamespaces: jest.fn().mockReturnValue(of(mockNamespaces)) + } + }, + { provide: AuthStorageService, useClass: MockAuthStorageService } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NvmeofSubsystemNamespacesListComponent); + component = fixture.componentInstance; + nvmeofService = TestBed.inject(NvmeofService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).not.toBeNull(); + expect(component).not.toBeUndefined(); + }); + + it('should list namespaces filtered by subsystem', fakeAsync(() => { + component.ngOnInit(); // Trigger ngOnInit + tick(); // wait for ngOnInit subscription + expect(nvmeofService.listNamespaces).toHaveBeenCalledWith( + 'group1', + 'nqn.2016-06.io.spdk:cnode1' + ); + expect(component.namespaces.length).toEqual(2); + expect(component.namespaces[0].nsid).toEqual(1); + })); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.ts new file mode 100644 index 00000000000..7f3957f6196 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.ts @@ -0,0 +1,187 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { CdTableAction } from '~/app/shared/models/cd-table-action'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { NvmeofSubsystemNamespace } from '~/app/shared/models/nvmeof'; +import { Permission } from '~/app/shared/models/permissions'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; +import { IopsPipe } from '~/app/shared/pipes/iops.pipe'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; +import { combineLatest, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +const BASE_URL = 'block/nvmeof/subsystems'; + +@Component({ + selector: 'cd-nvmeof-subsystem-namespaces-list', + templateUrl: './nvmeof-subsystem-namespaces-list.component.html', + styleUrls: ['./nvmeof-subsystem-namespaces-list.component.scss'], + standalone: false +}) +export class NvmeofSubsystemNamespacesListComponent implements OnInit, OnDestroy { + subsystemNQN: string; + group: string; + namespacesColumns: any; + tableActions: CdTableAction[]; + selection = new CdTableSelection(); + permission: Permission; + namespaces: NvmeofSubsystemNamespace[] = []; + + private destroy$ = new Subject(); + + constructor( + // ... constructor stays mostly same + public actionLabels: ActionLabelsI18n, + private router: Router, + private modalService: ModalCdsService, + private authStorageService: AuthStorageService, + private taskWrapper: TaskWrapperService, + private nvmeofService: NvmeofService, + private dimlessBinaryPipe: DimlessBinaryPipe, + private iopsPipe: IopsPipe, + private route: ActivatedRoute + ) { + this.permission = this.authStorageService.getPermissions().nvmeof; + } + + ngOnInit() { + combineLatest([this.route.parent?.params, this.route.queryParams]) + .pipe(takeUntil(this.destroy$)) + .subscribe(([params, qp]) => { + this.subsystemNQN = params['subsystem_nqn']; + this.group = qp['group']; + if (this.subsystemNQN && this.group) { + this.listNamespaces(); + } + }); + + this.setupColumns(); + this.setupTableActions(); + } + + setupColumns() { + this.namespacesColumns = [ + { + name: $localize`Namespace ID`, + prop: 'nsid' + }, + { + name: $localize`Pool`, + prop: 'rbd_pool_name', + flexGrow: 2 + }, + { + name: $localize`Image`, + prop: 'rbd_image_name', + flexGrow: 3 + }, + { + name: $localize`Image Size`, + prop: 'rbd_image_size', + pipe: this.dimlessBinaryPipe + }, + { + name: $localize`Block Size`, + prop: 'block_size', + pipe: this.dimlessBinaryPipe + }, + { + name: $localize`IOPS`, + prop: 'rw_ios_per_second', + sortable: false, + pipe: this.iopsPipe, + flexGrow: 1.5 + } + ]; + } + + setupTableActions() { + this.tableActions = [ + { + name: this.actionLabels.CREATE, + permission: 'create', + icon: Icons.add, + click: () => + this.router.navigate( + [BASE_URL, { outlets: { modal: [URLVerbs.CREATE, this.subsystemNQN, 'namespace'] } }], + { queryParams: { group: this.group } } + ), + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection + }, + { + name: this.actionLabels.EDIT, + permission: 'update', + icon: Icons.edit, + click: () => + this.router.navigate( + [ + BASE_URL, + { + outlets: { + modal: [ + URLVerbs.EDIT, + this.subsystemNQN, + 'namespace', + this.selection.first().nsid + ] + } + } + ], + { queryParams: { group: this.group } } + ) + }, + { + name: this.actionLabels.DELETE, + permission: 'delete', + icon: Icons.destroy, + click: () => this.deleteNamespaceModal() + } + ]; + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + listNamespaces() { + if (this.group) { + this.nvmeofService + .listNamespaces(this.group, this.subsystemNQN) + .pipe(takeUntil(this.destroy$)) + .subscribe((res: NvmeofSubsystemNamespace[]) => { + this.namespaces = res || []; + }); + } else { + this.namespaces = []; + } + } + + deleteNamespaceModal() { + const namespace = this.selection.first(); + this.modalService.show(DeleteConfirmationModalComponent, { + itemDescription: 'Namespace', + itemNames: [namespace.nsid], + actionDescription: 'delete', + submitActionObservable: () => + this.taskWrapper.wrapTaskAroundCall({ + task: new FinishedTask('nvmeof/namespace/delete', { + nqn: this.subsystemNQN, + nsid: namespace.nsid + }), + call: this.nvmeofService.deleteNamespace(this.subsystemNQN, namespace.nsid, this.group) + }) + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} 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 65f8efea793..ca9f7d7ef19 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 @@ -15,18 +15,20 @@ Listeners - - + + Namespaces - - + + 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 7de6ae958a5..32097cd044a 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 @@ -25,8 +25,6 @@ export type SubsystemPayload = { type StepResult = { step: string; success: boolean; error?: string }; -const PAGE_URL = 'block/nvmeof/subsystems'; - @Component({ selector: 'cd-nvmeof-subsystems-form', templateUrl: './nvmeof-subsystems-form.component.html', @@ -55,6 +53,7 @@ export class NvmeofSubsystemsFormComponent implements OnInit { title: string = $localize`Create Subsystem`; description: string = $localize`Subsytems define how hosts connect to NVMe namespaces and ensure secure access to storage.`; isSubmitLoading: boolean = false; + private lastCreatedNqn: string; @ViewChild(TearsheetComponent) tearsheet!: TearsheetComponent; @@ -76,6 +75,7 @@ export class NvmeofSubsystemsFormComponent implements OnInit { } onSubmit(payload: SubsystemPayload) { this.isSubmitLoading = true; + this.lastCreatedNqn = payload.nqn; const stepResults: StepResult[] = []; const initiatorRequest: InitiatorRequest = { host_nqn: payload.hostType === HOST_TYPE.ALL ? '*' : payload.addedHosts.join(','), @@ -117,7 +117,9 @@ export class NvmeofSubsystemsFormComponent implements OnInit { errorMsg ); this.isSubmitLoading = false; - this.router.navigate([PAGE_URL, { outlets: { modal: null } }]); + this.router.navigate(['block/nvmeof/gateways'], { + queryParams: { group: this.group, tab: 'subsystem' } + }); } }); } @@ -161,6 +163,12 @@ export class NvmeofSubsystemsFormComponent implements OnInit { : $localize`Subsystem created`; this.notificationService.show(type, title, sanitizedHtml); - this.router.navigate([PAGE_URL, { outlets: { modal: null } }]); + this.router.navigate(['block/nvmeof/gateways'], { + 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 737ba752660..20c797391b2 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 @@ -51,6 +51,17 @@
+ + + {{ value }} + + +
; + @ViewChild('customTableItemTemplate', { static: true }) + customTableItemTemplate: TemplateRef; + + @ViewChild('table') table: TableComponent; + + subsystems: (NvmeofSubsystem & { gw_group?: string; initiator_count?: number })[] = []; + pendingNqn: string = null; subsystemsColumns: any; permissions: Permissions; selection = new CdTableSelection(); @@ -70,7 +83,7 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit private route: ActivatedRoute, private modalService: ModalCdsService, private taskWrapper: TaskWrapperService, - private notificationService: NotificationService + private cdRef: ChangeDetectorRef ) { super(); this.permissions = this.authStorageService.getPermissions(); @@ -78,6 +91,7 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit ngOnInit() { this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => { + if (params?.['nqn']) this.pendingNqn = params['nqn']; if (params?.['group']) this.onGroupSelection({ content: params?.['group'] }); }); this.setGatewayGroups(); @@ -85,7 +99,8 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit { name: $localize`Subsystem NQN`, prop: 'nqn', - flexGrow: 2 + flexGrow: 2, + cellTemplate: this.customTableItemTemplate }, { name: $localize`Gateway group`, @@ -144,16 +159,15 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit return forkJoin(subs.map((sub) => this.enrichSubsystemWithInitiators(sub))); }), catchError((error) => { - this.notificationService.show( - NotificationType.error, - $localize`Unable to fetch Gateway group`, - $localize`Gateway group does not exist` - ); this.handleError(error); return of([]); }) ); }), + tap((subs) => { + this.subsystems = subs; + this.expandPendingSubsystem(); + }), takeUntil(this.destroy$) ); } @@ -220,8 +234,15 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit } updateGroupSelectionState() { - if (!this.group && this.gwGroups.length) { - this.onGroupSelection(this.gwGroups[0]); + if (this.gwGroups.length) { + if (!this.group) { + this.onGroupSelection(this.gwGroups[0]); + } else { + this.gwGroups = this.gwGroups.map((g) => ({ + ...g, + selected: g.content === this.group + })); + } this.gwGroupsEmpty = false; this.gwGroupPlaceholder = DEFAULT_PLACEHOLDER; } else { @@ -244,6 +265,19 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit this.context?.error?.(error); } + private expandPendingSubsystem() { + if (!this.pendingNqn) return; + const match = this.subsystems.find((s) => s.nqn === this.pendingNqn); + if (match && this.table) { + setTimeout(() => { + this.table.expanded = match; + this.table.toggleExpandRow(); + this.cdRef.detectChanges(); + }); + } + this.pendingNqn = null; + } + private enrichSubsystemWithInitiators(sub: NvmeofSubsystem) { return this.nvmeofService.getInitiators(sub.nqn, this.group).pipe( catchError(() => of([])), diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sidebar-layout/sidebar-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sidebar-layout/sidebar-layout.component.html index 6824aa2c16b..142c5be6db7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sidebar-layout/sidebar-layout.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sidebar-layout/sidebar-layout.component.html @@ -11,6 +11,7 @@ @for (item of items; track item.label) { {{ item.label }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sidebar-layout/sidebar-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sidebar-layout/sidebar-layout.component.ts index 971a872df31..8c4a9600c55 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sidebar-layout/sidebar-layout.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sidebar-layout/sidebar-layout.component.ts @@ -3,6 +3,7 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; export interface SidebarItem { label: string; route: string[]; + routeExtras?: any; routerLinkActiveOptions?: { exact: boolean }; } 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 159708f3190..b55207934b7 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 @@ -53,12 +53,14 @@ export interface NvmeofSubsystemNamespace { rbd_image_name: string; rbd_pool_name: string; load_balancing_group: number; - rbd_image_size: number; + rbd_image_size: number | string; block_size: number; - rw_ios_per_second: number; - rw_mbytes_per_second: number; - r_mbytes_per_second: number; - w_mbytes_per_second: number; + rw_ios_per_second: number | string; + rw_mbytes_per_second: number | string; + r_mbytes_per_second: number | string; + w_mbytes_per_second: number | string; + ns_subsystem_nqn?: string; // Field from JSON + subsystem_nqn?: string; // Keep for compatibility if needed, but JSON has ns_subsystem_nqn } export interface NvmeofGatewayGroup extends CephServiceSpec { diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss index 6f59c43b99d..f32fabfbcba 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss @@ -50,6 +50,10 @@ margin-top: layout.$spacing-03; } +.cds-mt-1 { + margin-top: layout.$spacing-01; +} + .cds-mt-5 { margin-top: layout.$spacing-05; }