From 4c790d72da61bd4331bb5845ae42a6f1d0454ad1 Mon Sep 17 00:00:00 2001 From: pujashahu Date: Mon, 15 Jun 2026 23:47:32 +0530 Subject: [PATCH] mgr/dashboard : Implement dependent resource handling for gateway Deletion Fixes: https://tracker.ceph.com/issues/77403 Signed-off-by: pujaoshahu --- .../src/app/ceph/block/block.module.ts | 4 +- ...ay-group-delete-guard-modal.component.html | 31 ++++++++++ ...ay-group-delete-guard-modal.component.scss | 0 ...group-delete-guard-modal.component.spec.ts | 55 ++++++++++++++++ ...eway-group-delete-guard-modal.component.ts | 29 +++++++++ .../nvmeof-gateway-group.component.html | 1 + .../nvmeof-gateway-group.component.spec.ts | 62 ++++++++++++++++++- .../nvmeof-gateway-group.component.ts | 35 +++++++++-- 8 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.ts 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 cb9b96abcd8..442c79a1d3d 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 @@ -105,6 +105,7 @@ import { NvmeSubsystemViewBreadcrumbResolver } from './nvme-subsystem-view/nvme- 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'; +import { NvmeofGatewayGroupDeleteGuardModalComponent } from './nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component'; @NgModule({ imports: [ @@ -194,7 +195,8 @@ import { NvmeofTabsComponent } from './nvmeof-tabs/nvmeof-tabs.component'; NvmeofSubsystemsStepFourComponent, NvmeofSubsystemOverviewComponent, NvmeofSubsystemPerformanceComponent, - NvmeofTabsComponent + NvmeofTabsComponent, + NvmeofGatewayGroupDeleteGuardModalComponent ], exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.html new file mode 100644 index 00000000000..433265ed544 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.html @@ -0,0 +1,31 @@ + + +

Manage {{ gatewayName }}

+ Can't delete {{ gatewayName }} +
+
+

+ This resource has connected items that must be deleted first. Delete the connected items, and try again. +

+

View connected items:

+
+ @for (sub of connectedSubsystems; track sub.nqn) { + + } +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.spec.ts new file mode 100644 index 00000000000..ff7ab9076b4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NvmeofGatewayGroupDeleteGuardModalComponent } from './nvmeof-gateway-group-delete-guard-modal.component'; + +describe('NvmeofGatewayGroupDeleteGuardModalComponent', () => { + let component: NvmeofGatewayGroupDeleteGuardModalComponent; + let fixture: ComponentFixture; + let router: Router; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [NvmeofGatewayGroupDeleteGuardModalComponent], + providers: [ + { provide: 'gatewayName', useValue: 'gateway-dev' }, + { + provide: 'connectedSubsystems', + useValue: [{ nqn: 'subsystem-1' }, { nqn: 'subsystem-2' }] + } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NvmeofGatewayGroupDeleteGuardModalComponent); + component = fixture.componentInstance; + router = TestBed.inject(Router); + jest.spyOn(router, 'navigate').mockImplementation(); + fixture.detectChanges(); + }); + + it('should create the modal component', () => { + expect(component).toBeTruthy(); + }); + + it('should load dynamic inputs', () => { + expect(component.gatewayName).toBe('gateway-dev'); + expect(component.connectedSubsystems).toEqual([{ nqn: 'subsystem-1' }, { nqn: 'subsystem-2' }]); + }); + + it('should navigate to subsystem detail page and close modal', () => { + const closeSpy = jest.spyOn(component, 'closeModal').mockImplementation(); + + component.navigateToSubsystem('subsystem-1'); + + expect(router.navigate).toHaveBeenCalledWith( + ['/block/nvmeof/subsystems', 'subsystem-1', 'overview'], + { queryParams: { group: 'gateway-dev' } } + ); + expect(closeSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.ts new file mode 100644 index 00000000000..86cfd478a44 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.ts @@ -0,0 +1,29 @@ +import { Component, Inject, Optional } from '@angular/core'; +import { Router } from '@angular/router'; +import { BaseModal } from 'carbon-components-angular'; + +interface ConnectedSubsystem { + nqn: string; +} + +@Component({ + selector: 'cd-nvmeof-gateway-group-delete-guard-modal', + templateUrl: './nvmeof-gateway-group-delete-guard-modal.component.html', + standalone: false +}) +export class NvmeofGatewayGroupDeleteGuardModalComponent extends BaseModal { + constructor( + private router: Router, + @Optional() @Inject('gatewayName') public gatewayName: string, + @Optional() @Inject('connectedSubsystems') public connectedSubsystems: ConnectedSubsystem[] = [] + ) { + super(); + } + + navigateToSubsystem(nqn: string): void { + this.router.navigate(['/block/nvmeof/subsystems', nqn, 'overview'], { + queryParams: { group: this.gatewayName } + }); + this.closeModal(); + } +} 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 7f48a42cbf6..eae963ce641 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 @@ -56,6 +56,7 @@ } + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.spec.ts index 9ea34f16bd5..69fad9acbfd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.spec.ts @@ -1,10 +1,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NvmeofGatewayGroupComponent } from './nvmeof-gateway-group.component'; -import { GridModule, TabsModule } from 'carbon-components-angular'; +import { GridModule, TabsModule, ModalModule } from 'carbon-components-angular'; import { NvmeofService } from '~/app/shared/api/nvmeof.service'; import { of } from 'rxjs'; import { HttpClientModule } from '@angular/common/http'; import { SharedModule } from '~/app/shared/shared.module'; +import { Router } from '@angular/router'; +import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; +import { NvmeofGatewayGroupDeleteGuardModalComponent } from './nvmeof-gateway-group-delete-guard-modal.component'; describe('NvmeofGatewayGroupComponent', () => { let component: NvmeofGatewayGroupComponent; @@ -18,9 +23,20 @@ describe('NvmeofGatewayGroupComponent', () => { }; await TestBed.configureTestingModule({ - imports: [HttpClientModule, SharedModule, TabsModule, GridModule], + imports: [HttpClientModule, SharedModule, TabsModule, GridModule, ModalModule], declarations: [NvmeofGatewayGroupComponent], - providers: [{ provide: NvmeofService, useValue: nvmeofServiceSpy }] + providers: [ + { provide: NvmeofService, useValue: nvmeofServiceSpy }, + { + provide: Router, + useValue: { navigate: jest.fn() } + }, + { + provide: ModalCdsService, + useValue: { show: jest.fn() } + } + ], + schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); fixture = TestBed.createComponent(NvmeofGatewayGroupComponent); @@ -242,4 +258,44 @@ describe('NvmeofGatewayGroupComponent', () => { done(); }); }); + + describe('Delete Flow with/without Subsystems', () => { + let mockGroup: any; + + beforeEach(() => { + mockGroup = { + service_name: 'nvmeof.rbd.default', + spec: { group: 'default' }, + subSystemCount: 0 + }; + component.selection.first = jest.fn().mockReturnValue(mockGroup); + }); + + it('should show can-not-delete modal if subsystems exist', () => { + const mockSubsystems = [{ nqn: 'subsystem-1' }, { nqn: 'subsystem-2' }]; + nvmeofService.listSubsystems.mockReturnValue(of(mockSubsystems)); + const modalService = TestBed.inject(ModalCdsService); + + component.deleteGatewayGroupModal(); + + expect(nvmeofService.listSubsystems).toHaveBeenCalledWith('default'); + expect(modalService.show).toHaveBeenCalledWith(NvmeofGatewayGroupDeleteGuardModalComponent, { + gatewayName: 'default', + connectedSubsystems: [{ nqn: 'subsystem-1' }, { nqn: 'subsystem-2' }] + }); + }); + + it('should show delete confirmation modal if no subsystems exist', () => { + nvmeofService.listSubsystems.mockReturnValue(of([])); + const modalService = TestBed.inject(ModalCdsService); + + component.deleteGatewayGroupModal(); + + expect(nvmeofService.listSubsystems).toHaveBeenCalledWith('default'); + expect(modalService.show).toHaveBeenCalledWith( + DeleteConfirmationModalComponent, + expect.any(Object) + ); + }); + }); }); 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 3ccef418bd0..933a085a1ff 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 @@ -13,7 +13,7 @@ import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; import { Permission } from '~/app/shared/models/permissions'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { Icons, IconSize } from '~/app/shared/enum/icons.enum'; -import { NvmeofGatewayGroup } from '~/app/shared/models/nvmeof'; +import { NvmeofGatewayGroup, NvmeofSubsystem } from '~/app/shared/models/nvmeof'; import { CephServiceSpec } from '~/app/shared/models/service.interface'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { CephServiceService } from '~/app/shared/api/ceph-service.service'; @@ -24,6 +24,7 @@ import { DeletionImpact } from '~/app/shared/enum/delete-confirmation-modal-impa import { NotificationService } from '~/app/shared/services/notification.service'; import { NotificationType } from '~/app/shared/enum/notification-type.enum'; import { URLBuilderService } from '~/app/shared/services/url-builder.service'; +import { NvmeofGatewayGroupDeleteGuardModalComponent } from './nvmeof-gateway-group-delete-guard-modal.component'; const BASE_URL = 'block/nvmeof/gateways'; @@ -194,16 +195,42 @@ export class NvmeofGatewayGroupComponent implements OnInit { spec: { group } } = selectedGroup; - const disableForm = selectedGroup.subSystemCount > 0 || !group; + if (!group) { + return; + } + + // Fetch actual subsystem list to decide which modal to show + this.nvmeofService + .listSubsystems(group) + .pipe(catchError(() => of([]))) + .subscribe((subsystems: NvmeofSubsystem[]) => { + let subsList: NvmeofSubsystem[] = []; + if (subsystems) { + const rawList = Array.isArray(subsystems) ? subsystems : [subsystems]; + subsList = rawList.filter((subsystem: NvmeofSubsystem) => subsystem && subsystem.nqn); + } + + if (subsList.length > 0) { + this.modalService.show(NvmeofGatewayGroupDeleteGuardModalComponent, { + gatewayName: group, + connectedSubsystems: subsList.map((subsystem: NvmeofSubsystem) => ({ + nqn: subsystem.nqn + })) + }); + } else { + // No subsystems — show the regular delete confirmation modal + this.showDeleteConfirmationModal(selectedGroup, serviceName); + } + }); + } + private showDeleteConfirmationModal(selectedGroup: CephServiceSpec, serviceName: string) { this.modalService.show(DeleteConfirmationModalComponent, { impact: DeletionImpact.high, itemDescription: $localize`gateway group`, bodyTemplate: this.deleteTpl, itemNames: [selectedGroup.spec.group], bodyContext: { - disableForm, - subsystemCount: selectedGroup.subSystemCount, deletionMessage: $localize`Deleting ${selectedGroup.spec.group} will remove all associated subsystems and may disrupt traffic routing for services relying on it. This action cannot be undone.` }, submitActionObservable: () => { -- 2.47.3