From: pujashahu Date: Wed, 26 Nov 2025 06:44:13 +0000 (+0530) Subject: mgr/dashboard: NVme-Delete Gateway group X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=a5f727a892803621a512b67f453058a7b98bb87b;p=ceph.git mgr/dashboard: NVme-Delete Gateway group Fixes: https://tracker.ceph.com/issues/73995 Signed-off-by: pujaoshahu --- 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 a06b87feb6708..8c629d76836de 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 @@ -12,6 +12,11 @@ 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> + + @@ -26,19 +31,30 @@ gap="4"> @if (gateway.running > 0) { - - + + {{ gateway.running }} - } + } - @if (gateway.error > 0) { - - + @if (gateway.error > 0) { + + {{ gateway.error }} - } - - + } + + @if (subsystemCount > 0) { + +

Cannot delete gateway group

+

This gateway group includes dependent NVMe-oF resources. Remove the associated initiators and gateway targets before proceeding with deletion.

+
+ } +
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 1c7ca0a3b88fc..5dcca32b006dd 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 @@ -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; + @ViewChild('deleteTpl', { static: true }) + deleteTpl: TemplateRef; + 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); + }) + ); + } + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.html index 0871c12f51335..20541a41536f7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.html @@ -12,24 +12,27 @@ novalidate>

{{ infoMessage }}

-

Are you sure that you want to {{ actionDescription | lowercase }} {{ itemNames[0] }}?

+ +

Are you sure that you want to {{ actionDescription | lowercase }} {{ itemNames[0] }}?

+

Are you sure that you want to {{ actionDescription | lowercase }} the selected items?

  • {{ itemName }}
-
+
-

Are you sure that you want to {{ actionDescription | lowercase }} the selected {{ itemDescription }}?

+

Are you sure that you want to {{ actionDescription | lowercase }} the selected {{ itemDescription }}?

@@ -43,26 +46,35 @@ i18n>Yes, I am sure. - + Deleting {{ itemNames[0] }} will remove all associated {{itemDescription}} and may disrupt traffic routing for + services relying on it. This action cannot be undone. +

+ + Type {{ itemNames[0] }} to confirm (required) + + Resource Name + [disabled]="deletionForm.controls.confirmInput.disabled"> + [attr.disabled]="deletionForm.controls.confirmInput.disabled ? true : null" + i18n-placeholder + autofocus/> - This field is required. - Enter the correct resource name. @@ -77,13 +89,25 @@ [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'"> - + @if (impact === impactEnum.high) { +

+ {{(actionDescription | titlecase) + ' ' + itemDescription}} +

+

+ Confirm delete +

+ } @else {

- {{ actionDescription | titlecase }} {{ itemDescription }} + {{(actionDescription | titlecase) + ' ' + itemDescription}}

+ }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.scss index 979cb13fe228f..3efe9b3506cf0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.scss @@ -3,7 +3,7 @@ } .modal-body label { - font-weight: bold; + font-weight: normal; } .modal-body .question .form-check { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.spec.ts index d64c979312117..a3c5c97d5ab52 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.spec.ts @@ -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, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.ts index dcdefd205af9d..66d0ad6c3a3ee 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.ts @@ -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; - + submitDisabled$: Observable = 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, - @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 index 0000000000000..546ac950fdd55 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-confirmation.model.ts @@ -0,0 +1,6 @@ +export interface DeleteConfirmationBodyContext { + warningMessage?: string; + disableForm?: boolean; + inputLabel?: string; + inputPlaceholder?: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 6a43e16f24d8c..8a7bdc6d04ad9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -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}`; } diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss index e113833067272..a64341c7709ca 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss @@ -11,3 +11,15 @@ .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; +}