]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard : Implement dependent resource handling for gateway Deletion 69487/head
authorpujashahu <pshahu@redhat.com>
Mon, 15 Jun 2026 18:17:32 +0000 (23:47 +0530)
committerpujashahu <pshahu@redhat.com>
Fri, 26 Jun 2026 09:56:37 +0000 (15:26 +0530)
Fixes: https://tracker.ceph.com/issues/77403
Signed-off-by: pujaoshahu <pshahu@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.ts

index cb9b96abcd8a77a1730587bf4585f978666f4d6f..442c79a1d3d7c75037b23850344f395392cbaa98 100644 (file)
@@ -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 (file)
index 0000000..433265e
--- /dev/null
@@ -0,0 +1,31 @@
+<cds-modal size="sm"
+           [open]="open"
+           (overlaySelected)="closeModal()">
+  <cds-modal-header (closeSelect)="closeModal()">
+    <h2 cdsModalHeaderLabel
+        modal-primary-focus
+        i18n>Manage {{ gatewayName }}</h2>
+    <span class="cds--type-heading-03"
+        cdsModalHeaderHeading
+        i18n>Can't delete {{ gatewayName }}</span>
+  </cds-modal-header>
+  <section cdsModalContent>
+    <p class="cds--type-body-01 cds--mb-05"
+       i18n>
+      This resource has connected items that must be deleted first. Delete the connected items, and try again.
+    </p>
+    <p class="cds--type-label-01 cds--mb-04"
+       i18n>View connected items:</p>
+    <div>
+      @for (sub of connectedSubsystems; track sub.nqn) {
+      <div class="cds--mb-04">
+        <a class="cds--link cds--type-body-01"
+           (click)="navigateToSubsystem(sub.nqn)">
+          <span class="cds--mr-03">{{ sub.nqn }}</span>
+          <cd-icon type="launch"></cd-icon>
+        </a>
+      </div>
+      }
+    </div>
+  </section>
+</cds-modal>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..ff7ab90
--- /dev/null
@@ -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<NvmeofGatewayGroupDeleteGuardModalComponent>;
+  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 (file)
index 0000000..86cfd47
--- /dev/null
@@ -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();
+  }
+}
index 7f48a42cbf633bb1071ef12cff4f059e489b49b0..eae963ce64152e6f0d5c448ce659754d48d0d764 100644 (file)
@@ -56,6 +56,7 @@
     }
   </div>
 </ng-template>
+
 <ng-template #deleteTpl
              let-groupName="groupName"
              let-subsystemCount="subsystemCount">
index 9ea34f16bd57e8009154334e83b9def4af75fd6b..69fad9acbfd5b385680343c32cf766476a371f79 100644 (file)
@@ -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)
+      );
+    });
+  });
 });
index 3ccef418bd08f4af86f5a96607ca802a2c9a8e51..933a085a1ffe21aa520e579a08b7d02c2dbcc360 100644 (file)
@@ -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 <strong>${selectedGroup.spec.group}</strong> will remove all associated subsystems and may disrupt traffic routing for services relying on it. This action cannot be undone.`
       },
       submitActionObservable: () => {