From 1599d7fd5028b037d37685ed85dd7bd68cb5bda8 Mon Sep 17 00:00:00 2001 From: Dnyaneshwari Talwekar Date: Thu, 16 Jul 2026 18:30:31 +0530 Subject: [PATCH] mgr/dashboard: Subvolume group and subvolume not listed in NFS Share form on edit. Signed-off-by: Dnyaneshwari Talwekar Fixes: https://tracker.ceph.com/issues/78281 --- .../nfs/nfs-form/nfs-form.component.spec.ts | 23 ++++++++ .../ceph/nfs/nfs-form/nfs-form.component.ts | 22 +++++--- .../nfs/nfs-list/nfs-list.component.spec.ts | 52 ++++++++++++++++++- .../ceph/nfs/nfs-list/nfs-list.component.ts | 26 ++++++---- 4 files changed, 104 insertions(+), 19 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts index e8a5ada4bfc0..a9d25ff3640e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts @@ -220,6 +220,29 @@ describe('NfsFormComponent', () => { }); }); + describe('edit mode CephFS subvolume fields', () => { + it('should populate subvolume group and subvolume from export path', () => { + component.isEdit = true; + spyOn(component['subvolgrpService'], 'get').and.returnValue(of([{ name: 'g1' }])); + spyOn(component['subvolService'], 'get').and.returnValue(of([{ name: 'sv1' }])); + + component.resolveModel({ + fsal: { name: 'CEPH', fs_name: 'testfs' }, + path: '/volumes/g1/sv1', + protocols: [3, 4], + transports: ['TCP'], + clients: [], + squash: 'none' + }); + + expect(component.nfsForm.get('subvolume_group').value).toBe('g1'); + expect(component.nfsForm.get('subvolume').value).toBe('sv1'); + expect(component['subvolgrpService'].get).toHaveBeenCalledWith('testfs'); + expect(component['subvolService'].get).toHaveBeenCalledWith('testfs', 'g1'); + expect(component.allsubvols).toEqual([{ name: 'sv1' }]); + }); + }); + describe('pathExistence', () => { beforeEach(() => { component['nfsService']['lsDir'] = jest.fn((): Observable => diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts index 5b214b873f93..b0e8f8f34276 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts @@ -138,7 +138,7 @@ export class NfsFormComponent extends CdForm implements OnInit { (params: { cluster_id: string; export_id: string; rgw_export_type?: string }) => { this.cluster_id = decodeURIComponent(params.cluster_id); this.export_id = decodeURIComponent(params.export_id); - if (params.rgw_export_type) { + if (params.rgw_export_type && this.storageBackend === SUPPORTED_FSAL.RGW) { this.nfsForm.get('rgw_export_type').setValue(params.rgw_export_type); if (params.rgw_export_type === RgwExportType.BUCKET) { this.setBucket(); @@ -186,11 +186,17 @@ export class NfsFormComponent extends CdForm implements OnInit { this.isDefaultSubvolumeGroup(); } - async getSubVol() { - const fs_name = this.nfsForm.getValue('fsal').fs_name; + async getSubVol(fsName?: string) { + const fs_name = fsName ?? this.nfsForm.getRawValue()?.fsal?.fs_name; const subvolgrp = this.nfsForm.getValue('subvolume_group'); - await this.setSubVolGrpPath(); + if (!fs_name || !subvolgrp) { + return; + } + + if (!this.isEdit) { + await this.setSubVolGrpPath(); + } (subvolgrp === this.defaultSubVolGroup ? this.subvolService.get(fs_name) @@ -422,12 +428,14 @@ export class NfsFormComponent extends CdForm implements OnInit { this.nfsForm.patchValue({ fsal: { - ...this.nfsForm.get('fsal').value, + ...this.nfsForm.getRawValue()?.fsal, fs_name: fsName }, - subvolumeGroup, + subvolume_group: subvolumeGroup, subvolume }); + this.setUpVolumeValidation(); + this.getSubVol(fsName); } resolveClusters(clusters: string[]) { @@ -725,7 +733,7 @@ export class NfsFormComponent extends CdForm implements OnInit { if (control.pristine || !control.value) { return of({ required: true }); } - const fsName = this.nfsForm.getValue('fsal').fs_name; + const fsName = this.nfsForm.getRawValue()?.fsal?.fs_name; return this.nfsService.lsDir(fsName, control.value).pipe( map((directory: Directory) => directory.paths.includes(control.value) === requiredExistenceResult diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts index 5758325a8e3f..403513dd1d57 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts @@ -16,8 +16,9 @@ import { TaskListService } from '~/app/shared/services/task-list.service'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed, expectItemTasks, PermissionHelper } from '~/testing/unit-test-helper'; import { NfsDetailsComponent } from '../nfs-details/nfs-details.component'; -import { NfsListComponent } from './nfs-list.component'; -import { SUPPORTED_FSAL } from '../models/nfs.fsal'; +import { NfsListComponent, RgwExportType } from './nfs-list.component'; +import { RGW_USER_EXPORT_PATH, SUPPORTED_FSAL } from '../models/nfs.fsal'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; describe('NfsListComponent', () => { let component: NfsListComponent; @@ -55,6 +56,53 @@ describe('NfsListComponent', () => { expect(component).toBeTruthy(); }); + describe('edit action routerLink', () => { + let editRouterLink: () => string | [string, { rgw_export_type: RgwExportType }]; + + beforeEach(() => { + editRouterLink = component.tableActions.find((action) => action.permission === 'update') + .routerLink as () => string | [string, { rgw_export_type: RgwExportType }]; + }); + + it('should return a plain string for CephFS edit (no route params)', () => { + component.fsal = SUPPORTED_FSAL.CEPH; + component.selection = new CdTableSelection([ + { cluster_id: 'mycluster', export_id: '42', path: '/volumes/g1/sv1' } + ]); + + const link = editRouterLink(); + + expect(typeof link).toBe('string'); + expect(link).toBe('/cephfs/nfs/edit/mycluster/42'); + }); + + it('should return array with rgw_export_type for RGW bucket edit', () => { + component.fsal = SUPPORTED_FSAL.RGW; + component.selection = new CdTableSelection([ + { cluster_id: 'rgw-cluster', export_id: '7', path: '/my-bucket' } + ]); + + const link = editRouterLink(); + + expect(Array.isArray(link)).toBe(true); + expect(link[0]).toBe('/rgw/nfs/edit/rgw-cluster/7'); + expect(link[1]).toEqual({ rgw_export_type: RgwExportType.BUCKET }); + }); + + it('should return array with rgw_export_type user for RGW user-level edit', () => { + component.fsal = SUPPORTED_FSAL.RGW; + component.selection = new CdTableSelection([ + { cluster_id: 'rgw-cluster', export_id: '8', path: RGW_USER_EXPORT_PATH } + ]); + + const link = editRouterLink(); + + expect(Array.isArray(link)).toBe(true); + expect(link[0]).toBe('/rgw/nfs/edit/rgw-cluster/8'); + expect(link[1]).toEqual({ rgw_export_type: RgwExportType.USER }); + }); + }); + describe('after ngOnInit', () => { beforeEach(() => { fixture.detectChanges(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts index 7ddee926273c..594758878691 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts @@ -109,17 +109,23 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr const editAction: CdTableAction = { permission: 'update', icon: Icons.edit, - routerLink: () => [ - `/${prefix}/nfs/edit/${getNfsUri()}`, - { - rgw_export_type: - this.fsal === SUPPORTED_FSAL.RGW && - !_.isEmpty(this.selection?.first()?.path) && - this.selection?.first()?.path !== RGW_USER_EXPORT_PATH - ? RgwExportType.BUCKET - : RgwExportType.USER + routerLink: () => { + const currentPrefix = getPathfromFsal(this.fsal); + const editUrl = `/${currentPrefix}/nfs/edit/${getNfsUri()}`; + if (this.fsal !== SUPPORTED_FSAL.RGW) { + return editUrl; } - ], + return [ + editUrl, + { + rgw_export_type: + !_.isEmpty(this.selection?.first()?.path) && + this.selection?.first()?.path !== RGW_USER_EXPORT_PATH + ? RgwExportType.BUCKET + : RgwExportType.USER + } + ]; + }, name: this.actionLabels.EDIT }; -- 2.47.3