]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: NVme-Delete Gateway group 66418/head
authorpujashahu <pshahu@redhat.com>
Wed, 26 Nov 2025 06:44:13 +0000 (12:14 +0530)
committerpujaoshahu <pshahu@redhat.com>
Mon, 12 Jan 2026 20:42:17 +0000 (02:12 +0530)
Fixes: https://tracker.ceph.com/issues/73995
Signed-off-by: pujaoshahu <pshahu@redhat.com>
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.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-confirmation.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss

index a06b87feb670855e7db54d14edeffc481f6477e0..8c629d76836de2e2c1ab4a7bfee4a013545aa92f 100644 (file)
     i18n-emptyStateTitle
     emptyStateMessage="Set up your first gateway group to start using NVMe over Fabrics. This will allow you to create high-performance block storage with NVMe/TCP protocol."
     i18n-emptyStateMessage>
+  <cd-table-actions class="table-actions"
+                    [permission]="permission"
+                    [selection]="selection"
+                    [tableActions]="tableActions">
+  </cd-table-actions>
   </cd-table>
 </ng-container>
 
        gap="4">
 
     @if (gateway.running > 0) {
-    <span>
-      <cd-icon   type="success"></cd-icon>
+    <span [ngClass]="gateway.error > 0 ? 'cds-mr-3' : ''">
+      <cd-icon type="success"></cd-icon>
       <span class="cds-ml-3">{{ gateway.running }}</span>
     </span>
-  }
+    }
 
-  @if (gateway.error > 0) {
-    <span class="cds-ml-3">
-      <cd-icon   type="error"></cd-icon>
+    @if (gateway.error > 0) {
+    <span>
+      <cd-icon type="error"></cd-icon>
       <span class="cds-ml-3">{{ gateway.error }}</span>
     </span>
-  }
-
-
+    }
   </div>
 </ng-template>
+<ng-template #deleteTpl
+             let-groupName="groupName"
+             let-subsystemCount="subsystemCount">
+  @if (subsystemCount > 0) {
+  <cd-alert-panel type="warning"
+                  spacingClass="cds-mb-3">
+    <h1 class="cds--type-heading-compact-01"
+        i18n>Cannot delete gateway group</h1>
+    <p class="cds--type-body-compact-01"
+       i18n>This gateway group includes dependent NVMe-oF resources. Remove the associated initiators and gateway targets before proceeding with deletion.</p>
+  </cd-alert-panel>
+  }
+</ng-template>
index 1c7ca0a3b88fcd1575b8000392559ea8186d0e49..5dcca32b006dd43e5e4ee42bb42c60944719e6b1 100644 (file)
@@ -1,6 +1,6 @@
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Component, OnInit, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
 import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs';
-import { catchError, map, switchMap } from 'rxjs/operators';
+import { catchError, map, switchMap, tap } from 'rxjs/operators';
 import { GatewayGroup, NvmeofService } from '~/app/shared/api/nvmeof.service';
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { TableComponent } from '~/app/shared/datatable/table/table.component';
@@ -13,12 +13,21 @@ 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 { 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 { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { DeletionImpact } from '~/app/shared/enum/delete-confirmation-modal-impact.enum';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 
 @Component({
   selector: 'cd-nvmeof-gateway-group',
   templateUrl: './nvmeof-gateway-group.component.html',
   styleUrls: ['./nvmeof-gateway-group.component.scss'],
-  standalone: false
+  standalone: false,
+  encapsulation: ViewEncapsulation.None
 })
 export class NvmeofGatewayGroupComponent implements OnInit {
   @ViewChild(TableComponent, { static: true })
@@ -30,6 +39,9 @@ export class NvmeofGatewayGroupComponent implements OnInit {
   @ViewChild('gatewayStatusTpl', { static: true })
   gatewayStatusTpl: TemplateRef<any>;
 
+  @ViewChild('deleteTpl', { static: true })
+  deleteTpl: TemplateRef<any>;
+
   permission: Permission;
   tableActions: CdTableAction[];
   columns: CdTableColumn[] = [];
@@ -48,7 +60,11 @@ export class NvmeofGatewayGroupComponent implements OnInit {
   constructor(
     public actionLabels: ActionLabelsI18n,
     private authStorageService: AuthStorageService,
-    private nvmeofService: NvmeofService
+    private nvmeofService: NvmeofService,
+    public modalService: ModalCdsService,
+    private cephServiceService: CephServiceService,
+    public taskWrapper: TaskWrapperService,
+    private notificationService: NotificationService
   ) {}
 
   ngOnInit(): void {
@@ -75,15 +91,39 @@ export class NvmeofGatewayGroupComponent implements OnInit {
       }
     ];
 
+    const createAction: CdTableAction = {
+      permission: 'create',
+      icon: Icons.add,
+      name: this.actionLabels.CREATE,
+      canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+    };
+
+    const deleteAction: CdTableAction = {
+      permission: 'delete',
+      icon: Icons.destroy,
+      click: () => this.deleteGatewayGroupModal(),
+      name: this.actionLabels.DELETE,
+      canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
+    };
+    this.tableActions = [createAction, deleteAction];
     this.gatewayGroup$ = this.subject.pipe(
       switchMap(() =>
         this.nvmeofService.listGatewayGroups().pipe(
           switchMap((gatewayGroups: GatewayGroup[][]) => {
             const groups = gatewayGroups?.[0] ?? [];
+            if (groups.length === 0) {
+              return of([]);
+            }
             return forkJoin(
-              groups.map((group: NvmeofGatewayGroup) =>
-                this.nvmeofService.listSubsystems(group.spec.group).pipe(
-                  catchError(() => of([])),
+              groups.map((group: NvmeofGatewayGroup) => {
+                const isRunning = (group.status?.running ?? 0) > 0;
+                const subsystemsObservable = isRunning
+                  ? this.nvmeofService
+                      .listSubsystems(group.spec.group)
+                      .pipe(catchError(() => of([])))
+                  : of([]);
+
+                return subsystemsObservable.pipe(
                   map((subs) => ({
                     ...group,
                     name: group.spec?.group,
@@ -91,13 +131,12 @@ export class NvmeofGatewayGroupComponent implements OnInit {
                       running: group.status?.running ?? 0,
                       error: (group.status?.size ?? 0) - (group.status?.running ?? 0)
                     },
-
                     subSystemCount: Array.isArray(subs) ? subs.length : 0,
                     gateWayNode: group.placement?.hosts?.length ?? 0,
                     created: group.status?.created ? new Date(group.status.created) : null
                   }))
-                )
-              )
+                );
+              })
             );
           }),
           catchError((error) => {
@@ -116,4 +155,48 @@ export class NvmeofGatewayGroupComponent implements OnInit {
   updateSelection(selection: CdTableSelection): void {
     this.selection = selection;
   }
+
+  deleteGatewayGroupModal() {
+    const selectedGroup = this.selection.first();
+    if (!selectedGroup) {
+      return;
+    }
+    const {
+      service_name: serviceName,
+      spec: { group }
+    } = selectedGroup;
+
+    const disableForm = selectedGroup.subSystemCount > 0 || !group;
+
+    this.modalService.show(DeleteConfirmationModalComponent, {
+      impact: DeletionImpact.high,
+      itemDescription: $localize`gateway group`,
+      bodyTemplate: this.deleteTpl,
+      itemNames: [selectedGroup.spec.group],
+      bodyContext: {
+        disableForm,
+        subsystemCount: selectedGroup.subSystemCount
+      },
+      submitActionObservable: () => {
+        return this.taskWrapper
+          .wrapTaskAroundCall({
+            task: new FinishedTask('nvmeof/gateway/delete', { group: selectedGroup.spec.group }),
+            call: this.cephServiceService.delete(serviceName)
+          })
+          .pipe(
+            tap(() => {
+              this.table.refreshBtn();
+            }),
+            catchError((error) => {
+              this.table.refreshBtn();
+              this.notificationService.show(
+                NotificationType.error,
+                $localize`${`Failed to delete gateway group ${selectedGroup.spec.group}: ${error.message}`}`
+              );
+              return of(null);
+            })
+          );
+      }
+    });
+  }
 }
index 0871c12f5133526f11d0e574e1d33f1cb8c23644..20541a41536f73d5c900afb58a31b408488d6489 100644 (file)
           novalidate>
       <cd-alert-panel *ngIf="infoMessage"
                       type="info"
-                      spacingClass="mb-3"
+                      spacingClass="cds-mb-3"
                       i18n>
         <p>{{ infoMessage }}</p>
       </cd-alert-panel>
       <ng-container *ngTemplateOutlet="bodyTemplate; context: bodyContext"></ng-container>
       <div class="question">
         <span *ngIf="itemNames; else noNames">
-          <p *ngIf="itemNames.length === 1; else manyNames"
-             i18n>Are you sure that you want to {{ actionDescription | lowercase }} <strong>{{ itemNames[0] }}</strong>?</p>
+          <ng-container *ngIf="itemNames.length === 1; else manyNames">
+            <p *ngIf="impact == impactEnum.medium"
+               i18n>Are you sure that you want to {{ actionDescription | lowercase }} <strong>{{ itemNames[0] }}</strong>?</p>
+          </ng-container>
           <ng-template #manyNames>
             <p i18n>Are you sure that you want to {{ actionDescription | lowercase }} the selected items?</p>
             <ul>
               <li *ngFor="let itemName of itemNames"><strong>{{ itemName }}</strong></li>
             </ul>
-          </ng-template >
+          </ng-template>
         </span>
         <ng-template #noNames>
-          <p i18n>Are you sure that you want to {{ actionDescription | lowercase }} the selected {{ itemDescription }}?</p>
+          <p *ngIf="impact == impactEnum.medium"
+             i18n>Are you sure that you want to {{ actionDescription | lowercase }} the selected {{ itemDescription }}?</p>
         </ng-template>
         <ng-container *ngTemplateOutlet="childFormGroupTemplate; context:{form:deletionForm}"></ng-container>
         <div class="form-item">
                           i18n>Yes, I am sure.</cds-checkbox>
           </ng-container>
           <ng-template #highImpactDeletion>
-            <cds-text-label label="Resource Name"
+            <p i18n
+               class="cds--type-body-01">
+              Deleting <strong>{{ itemNames[0] }}</strong> will remove all associated {{itemDescription}} and may disrupt traffic routing for
+              services relying on it. This action cannot be undone.
+            </p>
+            <ng-template #labelTemplate>
+              <span i18n>Type <strong>{{ itemNames[0] }}</strong> to confirm (required)</span>
+            </ng-template>
+            <cds-text-label [labelTemplate]="labelTemplate"
                             labelInputID="resource_name"
-                            cdRequiredField="Resource Name"
-                            [invalid]="!deletionForm.controls.confirmInput.valid && deletionForm.controls.confirmInput.dirty"
+                            [cdRequiredField]="'Type ' + itemNames[0] + ' to confirm'"
+                            [invalid]="!deletionForm.controls.confirmInput.valid && deletionForm.controls.confirmInput.touched"
                             [invalidText]="ResourceError"
-                            i18n
-                            i18n-label>Resource Name
+                            [disabled]="deletionForm.controls.confirmInput.disabled">
               <input cdsText
                      type="text"
-                     placeholder="Enter resource name to delete"
+                     [placeholder]="'Name of resource'"
                      id="resource_name"
                      formControlName="confirmInput"
-                     i18n-placeholder/>
+                     [attr.disabled]="deletionForm.controls.confirmInput.disabled ? true : null"
+                     i18n-placeholder
+                     autofocus/>
             </cds-text-label>
             <ng-template #ResourceError>
-              <span *ngIf="deletionForm.showError('confirmInput', formDir, 'required')"
+              <span *ngIf="deletionForm.controls.confirmInput.hasError('required')"
                     class="invalid-feedback">
                 <ng-container i18n>This field is required.</ng-container>
               </span>
-              <span *ngIf="deletionForm.showError('confirmInput', formDir, 'matchResource')"
+              <span *ngIf="deletionForm.controls.confirmInput.hasError('matchResource')"
                     class="invalid-feedback">
                 <ng-container i18n>Enter the correct resource name.</ng-container>
               </span>
                         [form]="deletionForm"
                         [submitText]="(actionDescription | titlecase) + ' ' + itemDescription"
                         [modalForm]="true"
+                        [disabled]="(submitDisabled$ | async) || deletionForm.disabled || (impact === impactEnum.high ? !deletionForm.controls.confirmInput.value : !deletionForm.controls.confirmation.value)"
                         [submitBtnType]="(actionDescription === 'delete' || actionDescription === 'remove') ? 'danger' : 'primary'"></cd-form-button-panel>
 
 </cds-modal>
-
 <ng-template #deletionHeading>
+  @if (impact === impactEnum.high) {
+  <h2 cdsModalHeaderLabel
+      i18n>
+      {{(actionDescription | titlecase) + ' ' + itemDescription}}
+  </h2>
+  <h2 class="cds--type-heading-03"
+      cdsModalHeaderHeading
+      i18n>
+    Confirm delete
+  </h2>
+  } @else {
   <h3 cdsModalHeaderHeading
       i18n>
-    {{ actionDescription | titlecase }} {{ itemDescription }}
+      {{(actionDescription | titlecase) + ' ' + itemDescription}}
   </h3>
+  }
 </ng-template>
index d64c97931211710ac59ba643d6c4a1015cf9b256..a3c5c97d5ab522de79befd75ca927de1cb0828d9 100644 (file)
@@ -9,7 +9,7 @@ import { configureTestBed, modalServiceShow } from '~/testing/unit-test-helper';
 import { AlertPanelComponent } from '../alert-panel/alert-panel.component';
 import { LoadingPanelComponent } from '../loading-panel/loading-panel.component';
 import { DeleteConfirmationModalComponent } from './delete-confirmation-modal.component';
-import { ModalService, PlaceholderService } from 'carbon-components-angular';
+import { ModalService, PlaceholderService, CheckboxModule } from 'carbon-components-angular';
 import { ModalCdsService } from '../../services/modal-cds.service';
 import { DeletionImpact } from '../../enum/delete-confirmation-modal-impact.enum';
 
@@ -98,7 +98,7 @@ describe('DeleteConfirmationModalComponent', () => {
       AlertPanelComponent
     ],
     schemas: [NO_ERRORS_SCHEMA],
-    imports: [ReactiveFormsModule, MockModule, DirectivesModule],
+    imports: [ReactiveFormsModule, MockModule, DirectivesModule, CheckboxModule],
     providers: [
       ModalService,
       PlaceholderService,
index dcdefd205af9d57a5d263bc747b9ce08df121306..66d0ad6c3a3ee477347ef5f5b905b439a4042220 100644 (file)
@@ -1,12 +1,14 @@
 import { Component, Inject, OnInit, Optional, TemplateRef, ViewChild } from '@angular/core';
 import { UntypedFormControl, AbstractControl, ValidationErrors, Validators } from '@angular/forms';
-import { Observable } from 'rxjs';
+import { Observable, of } from 'rxjs';
+import { map, startWith } from 'rxjs/operators';
 
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
 import { SubmitButtonComponent } from '../submit-button/submit-button.component';
 import { BaseModal } from 'carbon-components-angular';
 import { CdValidators } from '../../forms/cd-validators';
 import { DeletionImpact } from '../../enum/delete-confirmation-modal-impact.enum';
+import { DeleteConfirmationBodyContext } from '../../models/delete-confirmation.model';
 
 @Component({
   selector: 'cd-deletion-modal',
@@ -21,7 +23,7 @@ export class DeleteConfirmationModalComponent extends BaseModal implements OnIni
   impactEnum = DeletionImpact;
   childFormGroup: CdFormGroup;
   childFormGroupTemplate: TemplateRef<any>;
-
+  submitDisabled$: Observable<boolean> = of(false);
   constructor(
     @Optional() @Inject('impact') public impact: DeletionImpact,
     @Optional() @Inject('itemDescription') public itemDescription: 'entry',
@@ -30,7 +32,10 @@ export class DeleteConfirmationModalComponent extends BaseModal implements OnIni
     @Optional() @Inject('submitAction') public submitAction?: Function,
     @Optional() @Inject('backAction') public backAction?: Function,
     @Optional() @Inject('bodyTemplate') public bodyTemplate?: TemplateRef<any>,
-    @Optional() @Inject('bodyContext') public bodyContext?: object,
+    @Optional() @Inject('subHeading') public subHeading?: string,
+    @Optional()
+    @Inject('bodyContext')
+    public bodyContext?: DeleteConfirmationBodyContext,
     @Optional() @Inject('infoMessage') public infoMessage?: string,
     @Optional()
     @Inject('submitActionObservable')
@@ -57,13 +62,16 @@ export class DeleteConfirmationModalComponent extends BaseModal implements OnIni
           )
         ]
       }),
-      confirmInput: new UntypedFormControl('', [
-        CdValidators.composeIf({ impact: this.impactEnum.high }, [
-          this.matchResourceName.bind(this),
-          Validators.required
-        ])
-      ])
+      confirmInput: new UntypedFormControl('', {
+        validators: [
+          CdValidators.composeIf({ impact: this.impactEnum.high }, [
+            this.matchResourceName.bind(this),
+            Validators.required
+          ])
+        ]
+      })
     };
+
     if (this.childFormGroup) {
       controls['child'] = this.childFormGroup;
     }
@@ -71,9 +79,27 @@ export class DeleteConfirmationModalComponent extends BaseModal implements OnIni
     if (!(this.submitAction || this.submitActionObservable)) {
       throw new Error('No submit action defined');
     }
+    if (this.bodyContext?.disableForm) {
+      this.toggleFormControls(this.bodyContext?.disableForm);
+      return;
+    }
+
+    if (this.impact === this.impactEnum.high && this.itemNames?.[0]) {
+      const target = String(this.itemNames[0]);
+      const confirmControl = this.deletionForm.controls.confirmInput;
+
+      this.submitDisabled$ = confirmControl.valueChanges.pipe(
+        startWith(confirmControl.value),
+        map((value: string) => value !== target)
+      );
+    }
   }
 
   matchResourceName(control: AbstractControl): ValidationErrors | null {
+    if (!control.value) {
+      return null;
+    }
+
     if (this.itemNames && control.value !== String(this.itemNames?.[0])) {
       return { matchResource: true };
     }
@@ -109,4 +135,15 @@ export class DeleteConfirmationModalComponent extends BaseModal implements OnIni
   stopLoadingSpinner() {
     this.deletionForm.setErrors({ cdSubmitButton: true });
   }
+
+  toggleFormControls(disableForm = false) {
+    if (disableForm) {
+      this.deletionForm.disable();
+      this.deletionForm.setErrors({ disabledByContext: true });
+      this.submitDisabled$ = of(true);
+    } else {
+      this.deletionForm.enable();
+      this.deletionForm.setErrors(null);
+    }
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-confirmation.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-confirmation.model.ts
new file mode 100644 (file)
index 0000000..546ac95
--- /dev/null
@@ -0,0 +1,6 @@
+export interface DeleteConfirmationBodyContext {
+  warningMessage?: string;
+  disableForm?: boolean;
+  inputLabel?: string;
+  inputPlaceholder?: string;
+}
index 6a43e16f24d8cebae1d074d4c53e993d6b7d953c..8a7bdc6d04ad9435a36d172a66fb103d13126d9a 100644 (file)
@@ -375,6 +375,9 @@ export class TaskMessageService {
       this.iscsiTarget(metadata)
     ),
     // nvmeof
+    'nvmeof/gateway/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+      this.nvmeofGateway(metadata)
+    ),
     'nvmeof/subsystem/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
       this.nvmeofSubsystem(metadata)
     ),
@@ -583,7 +586,9 @@ export class TaskMessageService {
   nvmeofSubsystem(metadata: any) {
     return $localize`subsystem '${metadata.nqn}'`;
   }
-
+  nvmeofGateway(metadata: any) {
+    return $localize`Gateway group '${metadata.group}'`;
+  }
   nvmeofListener(metadata: any) {
     return $localize`listener '${metadata.host_name} for subsystem ${metadata.nqn}`;
   }
index e113833067272d4b3d642d02bd5c20db88651acc..a64341c7709ca820924bebe83ce3e5424be39cb6 100644 (file)
 .cds-ml-3 {
   margin-left: layout.$spacing-03;
 }
+
+.cds-mr-3 {
+  margin-right: layout.$spacing-03;
+}
+
+.cds-mb-2 {
+  margin-bottom: layout.$spacing-02;
+}
+
+.cds-mb-3 {
+  margin-bottom: layout.$spacing-03;
+}