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: [
NvmeofSubsystemsStepFourComponent,
NvmeofSubsystemOverviewComponent,
NvmeofSubsystemPerformanceComponent,
- NvmeofTabsComponent
+ NvmeofTabsComponent,
+ NvmeofGatewayGroupDeleteGuardModalComponent
],
exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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();
+ }
+}
}
</div>
</ng-template>
+
<ng-template #deleteTpl
let-groupName="groupName"
let-subsystemCount="subsystemCount">
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;
};
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);
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)
+ );
+ });
+ });
});
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';
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';
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: () => {