From: Afreen Misbah Date: Tue, 28 Jul 2026 20:49:27 +0000 (+0530) Subject: mgr/dashboard: Pool deletion enhancements X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=d0add82594788f094d9dbf1a5543251b09b8dfe5;p=ceph.git mgr/dashboard: Pool deletion enhancements Admin role: - show confirmation modal that pool deletion is disabled - allow enabling pool deletion - fix pool deletion text Pool mgr role - show a modal that pool deletion is disabled Signed-off-by: Afreen Misbah --- diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts index cd87b245d993..7c27649b3474 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts @@ -64,6 +64,32 @@ export class PoolPageHelper extends PageHelper { .should('have.value', bpsLimit); } + private ensurePoolDeletionEnabled(name: string) { + this.clickRowActionButton(name, 'delete'); + cy.get('cds-modal').then(($modal) => { + if ($modal.text().includes("Can't delete")) { + cy.get('cds-modal button').contains('Enable').click({ force: true }); + cy.get('cds-modal').should('not.exist'); + } else { + cy.get('cds-modal .cds--modal-close-button button').click({ force: true }); + cy.get('cds-modal').should('not.exist'); + } + }); + } + + delete( + name: string, + columnIndex?: number, + section?: string, + cdsModal = true, + isMultiselect = false, + shouldReload = false, + confirmInput = true + ) { + this.ensurePoolDeletionEnabled(name); + super.delete(name, columnIndex, section, cdsModal, isMultiselect, shouldReload, confirmInput); + } + private setApplications(apps: string[]) { if (!apps || apps.length === 0) { return; 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 85048dc8d7ef..07c3068df468 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 @@ -109,7 +109,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'; + import { NvmeofSetupCardsComponent } from './nvmeof-setup-cards/nvmeof-setup-cards.component'; import { NvmeofGatewayGroupFilterComponent } from './nvmeof-gateway-group-filter/nvmeof-gateway-group-filter.component'; import { NvmeofEditAuthenticationComponent } from './nvmeof-edit-authentication/nvmeof-edit-authentication.component'; @@ -207,7 +207,6 @@ import { NvmeofEditAuthenticationComponent } from './nvmeof-edit-authentication/ NvmeofSubsystemOverviewComponent, NvmeofSubsystemPerformanceComponent, NvmeofTabsComponent, - NvmeofGatewayGroupDeleteGuardModalComponent, NvmeofEditAuthenticationComponent ], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.html index 0be274520ec9..ce8012fac530 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.html @@ -3,8 +3,8 @@ Please consult the  on how to configure and enable diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-authentication/nvmeof-edit-authentication.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-authentication/nvmeof-edit-authentication.component.html index 791390cd59da..a6cbce3aaa55 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-authentication/nvmeof-edit-authentication.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-authentication/nvmeof-edit-authentication.component.html @@ -13,8 +13,8 @@ @if (showAuthAlert) { 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 deleted file mode 100644 index 83151424e916..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.html +++ /dev/null @@ -1,51 +0,0 @@ - - -

- 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 deleted file mode 100644 index e69de29bb2d1..000000000000 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 deleted file mode 100644 index ff7ab9076b44..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 86cfd478a440..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group-delete-guard-modal.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -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.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.spec.ts index 93055deeddfb..32e558713423 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 @@ -8,7 +8,7 @@ import { HttpClientModule } from '@angular/common/http'; import { SharedModule } from '~/app/shared/shared.module'; 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'; +import { DeleteGuardModalComponent } from '~/app/shared/components/delete-guard-modal/delete-guard-modal.component'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { NvmeofStateService } from '../nvmeof-state.service'; @@ -300,9 +300,21 @@ describe('NvmeofGatewayGroupComponent', () => { component.deleteGatewayGroupModal(); expect(nvmeofService.listSubsystems).toHaveBeenCalledWith('default'); - expect(modalService.show).toHaveBeenCalledWith(NvmeofGatewayGroupDeleteGuardModalComponent, { - gatewayName: 'default', - connectedSubsystems: [{ nqn: 'subsystem-1' }, { nqn: 'subsystem-2' }] + expect(modalService.show).toHaveBeenCalledWith(DeleteGuardModalComponent, { + resourceName: 'default', + resourceType: 'gateway group', + connectedItems: [ + { + name: 'subsystem-1', + route: ['/block/nvmeof/subsystems', 'subsystem-1', 'overview'], + queryParams: { group: 'default' } + }, + { + name: 'subsystem-2', + route: ['/block/nvmeof/subsystems', 'subsystem-2', 'overview'], + queryParams: { group: 'default' } + } + ] }); }); 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 0c436fa10d6c..69ad15e67128 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 @@ -31,7 +31,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'; +import { DeleteGuardModalComponent } from '~/app/shared/components/delete-guard-modal/delete-guard-modal.component'; import { NvmeofStateService } from '../nvmeof-state.service'; const BASE_URL = 'block/nvmeof/gateways'; @@ -242,10 +242,13 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy { } if (subsList.length > 0) { - this.modalService.show(NvmeofGatewayGroupDeleteGuardModalComponent, { - gatewayName: group, - connectedSubsystems: subsList.map((subsystem: NvmeofSubsystem) => ({ - nqn: subsystem.nqn + this.modalService.show(DeleteGuardModalComponent, { + resourceName: group, + resourceType: $localize`gateway group`, + connectedItems: subsList.map((subsystem: NvmeofSubsystem) => ({ + name: subsystem.nqn, + route: ['/block/nvmeof/subsystems', subsystem.nqn, 'overview'], + queryParams: { group } })) }); } else { @@ -261,9 +264,7 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy { itemDescription: $localize`gateway group`, bodyTemplate: this.deleteTpl, itemNames: [selectedGroup.spec.group], - bodyContext: { - 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.` - }, + hasAssociatedResources: true, submitActionObservable: () => { return this.taskWrapper .wrapTaskAroundCall({ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.spec.ts index 976c8e396008..09ca8c4c34ef 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.spec.ts @@ -456,11 +456,7 @@ describe('NvmeofGatewayNodeComponent', () => { actionDescription: 'remove', hideDefaultWarning: true, impact: DeletionImpact.high, - - bodyContext: { - deletionMessage: - 'Removing host1 will detach it from the gateway group and stop handling new I/O requests. Active connections may be disrupted.

You can re-add this node later if required.' - }, + hasAssociatedResources: true, submitActionObservable: jasmine.any(Function) }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts index 89fe4865356f..01e4f48c88ee 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts @@ -224,9 +224,7 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges actionDescription: $localize`remove`, hideDefaultWarning: true, impact: DeletionImpact.high, - bodyContext: { - deletionMessage: $localize`Removing ${hostname} will detach it from the gateway group and stop handling new I/O requests. Active connections may be disrupted.

You can re-add this node later if required.` - }, + hasAssociatedResources: true, submitActionObservable: () => { const updatedSpec = this.buildRemoveGatewaySpecPayload(hostname); if (!updatedSpec) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts index 872d004ad738..c5771fbf9bef 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts @@ -237,15 +237,12 @@ export class NvmeofInitiatorsListComponent implements OnInit, OnDestroy { hostNQNs.splice(allowAllHostIndex, 1); itemNames = [...hostNQNs, $localize`Allow any host(*)`]; } - const hostName = itemNames[0]; const deleteModalRef = this.modalService.show(DeleteConfirmationModalComponent, { itemDescription: $localize`host`, impact: DeletionImpact.high, itemNames, actionDescription: 'remove', - bodyContext: { - deletionMessage: $localize`Removing ${hostName} will disconnect it and revoke its permissions for the ${this.subsystemNQN} subsystem.` - }, + hasAssociatedResources: true, submitActionObservable: () => this.taskWrapper.wrapTaskAroundCall({ task: new FinishedTask('nvmeof/initiator/remove', { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html index 7e91024675e0..617b7c98ba73 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html @@ -1,9 +1,9 @@ Currently, there are no listeners available in the NVMe subsystem. Please check your diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.ts index 4a8283eed555..6cc064f5096a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.ts @@ -264,9 +264,7 @@ export class NvmeofNamespacesListComponent implements OnInit, OnDestroy { bodyTemplate: this.deleteTpl, itemNames: [namespace.nsid], actionDescription: 'delete', - bodyContext: { - deletionMessage: $localize`Deleting the namespace ${namespace.nsid} will permanently remove all resources, services, and configurations within it. This action cannot be undone.` - }, + hasAssociatedResources: true, submitActionObservable: () => this.taskWrapper .wrapTaskAroundCall({ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.html index 33f58c990577..066998052c8d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.html @@ -52,11 +52,11 @@ > @if (formGroup.get('hostType').value === HOST_TYPE.ALL) { Allowing all hosts grants access to every initiator on the network. Authentication is not supported in this mode, which may expose the subsystem to unauthorized access. diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts index de0e7ebd4ccb..86203156de97 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts @@ -196,8 +196,8 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit bodyTemplate: this.deleteTpl, itemNames: [subsystem.nqn], actionDescription: 'delete', + hasAssociatedResources: true, bodyContext: { - deletionMessage: $localize`Deleting ${subsystem.nqn} will remove all associated configurations and resources. Dependent services may stop working. This action cannot be undone.`, forceDeleteAcknowledgementMessage: $localize`I understand this may remove resources still attached to this subsystem.` }, submitActionObservable: () => diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.html index 417ecd4e79f3..78e8ae894714 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.html @@ -291,8 +291,8 @@ Please start the loki and alloy services to see these logs. diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html index 8a2fda126ca5..95c9d163dc9a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html @@ -84,3 +84,22 @@ {{ row.pool_name }} + + +

+ To delete this pool, first enable pool deletion. +

+
+ + +

+ Pool deletion is currently disabled.
+ You do not have permissions to enable. Please contact administrator. +

+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts index 531dd8c53c4e..5973006b7eef 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts @@ -6,7 +6,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { RbdConfigurationListComponent } from '~/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component'; import { PgCategoryService } from '~/app/ceph/shared/pg-category.service'; @@ -104,7 +104,7 @@ describe('PoolListComponent', () => { spyOn(configurationService, 'get').and.returnValue(of(configOption)); fixture = TestBed.createComponent(PoolListComponent); component = fixture.componentInstance; - expect(component.monAllowPoolDelete).toBe(true); + expect(component.monAllowPoolDelete$.value).toBe(true); }); it('should set value correctly if mon_allow_pool_delete flag is set to false', () => { @@ -120,7 +120,7 @@ describe('PoolListComponent', () => { spyOn(configurationService, 'get').and.returnValue(of(configOption)); fixture = TestBed.createComponent(PoolListComponent); component = fixture.componentInstance; - expect(component.monAllowPoolDelete).toBe(false); + expect(component.monAllowPoolDelete$.value).toBe(false); }); it('should set value correctly if mon_allow_pool_delete flag is not set', () => { @@ -130,14 +130,14 @@ describe('PoolListComponent', () => { spyOn(configurationService, 'get').and.returnValue(of(configOption)); fixture = TestBed.createComponent(PoolListComponent); component = fixture.componentInstance; - expect(component.monAllowPoolDelete).toBe(false); + expect(component.monAllowPoolDelete$.value).toBe(false); }); - it('should set value correctly w/o config-opt read privileges', () => { + it('should set value to false w/o config-opt read privileges', () => { configOptRead = false; fixture = TestBed.createComponent(PoolListComponent); component = fixture.componentInstance; - expect(component.monAllowPoolDelete).toBe(true); + expect(component.monAllowPoolDelete$.value).toBe(false); }); }); @@ -480,21 +480,30 @@ describe('PoolListComponent', () => { }); }); - describe('getDisableDesc', () => { + describe('enablePoolDeletion', () => { + let configurationService: ConfigurationService; + beforeEach(() => { - component.selection.selected = [{ pool_name: 'foo' }]; + configurationService = TestBed.inject(ConfigurationService); }); - it('should return message if mon_allow_pool_delete flag is set to false', () => { - component.monAllowPoolDelete = false; - expect(component.getDisableDesc()).toBe( - 'Pool deletion is disabled by the mon_allow_pool_delete configuration setting.' - ); + it('should call configurationService.create and update monAllowPoolDelete$', () => { + spyOn(configurationService, 'create').and.returnValue(of(null)); + component.monAllowPoolDelete$.next(false); + component.enablePoolDeletion(); + expect(configurationService.create).toHaveBeenCalledWith({ + name: 'mon_allow_pool_delete', + value: [{ section: 'mon', value: true }], + force_update: false + }); + expect(component.monAllowPoolDelete$.value).toBe(true); }); - it('should return false if mon_allow_pool_delete flag is set to true', () => { - component.monAllowPoolDelete = true; - expect(component.getDisableDesc()).toBeFalsy(); + it('should not update monAllowPoolDelete$ on error', () => { + spyOn(configurationService, 'create').and.returnValue(throwError(() => 'fail')); + component.monAllowPoolDelete$.next(false); + component.enablePoolDeletion(); + expect(component.monAllowPoolDelete$.value).toBe(false); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts index 4b54fccc0a42..056cf2ea710f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import _ from 'lodash'; +import { BehaviorSubject } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; import { PgCategoryService } from '~/app/ceph/shared/pg-category.service'; @@ -9,7 +10,9 @@ import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile import { PoolService } from '~/app/shared/api/pool.service'; import { ListWithDetails } from '~/app/shared/classes/list-with-details.class'; import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache'; +import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component'; import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; +import { DeleteGuardModalComponent } from '~/app/shared/components/delete-guard-modal/delete-guard-modal.component'; import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { TableComponent } from '~/app/shared/datatable/table/table.component'; import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; @@ -32,6 +35,7 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { DeletionImpact } from '~/app/shared/enum/delete-confirmation-modal-impact.enum'; const BASE_URL = 'pool'; +const POOL_CONFIG = 'mon_allow_pool_delete'; interface PoolTaskMetadata { pool_name: string; @@ -56,6 +60,12 @@ export class PoolListComponent extends ListWithDetails implements OnInit { @ViewChild('poolConfigurationSourceTpl') poolConfigurationSourceTpl!: TemplateRef; + @ViewChild('poolEnableTpl', { static: true }) + poolEnableTpl!: TemplateRef; + + @ViewChild('poolNoPermsTpl', { static: true }) + poolNoPermsTpl!: TemplateRef; + pools: Pool[] = []; columns: CdTableColumn[] = []; selection = new CdTableSelection(); @@ -64,7 +74,7 @@ export class PoolListComponent extends ListWithDetails implements OnInit { tableActions: CdTableAction[] = []; tableStatus = new TableStatusViewCache(); cacheTiers: any[] = []; - monAllowPoolDelete = false; + monAllowPoolDelete$ = new BehaviorSubject(false); ecProfileList: ErasureCodeProfile[] = []; viewUrl = '/pool/view'; @@ -100,30 +110,21 @@ export class PoolListComponent extends ListWithDetails implements OnInit { permission: 'delete', icon: Icons.destroy, click: () => this.deletePoolModal(), - name: this.actionLabels.DELETE, - disable: this.getDisableDesc.bind(this) + name: this.actionLabels.DELETE } ]; // Note, we need read permissions to get the 'mon_allow_pool_delete' // configuration option. if (this.permissions.configOpt.read) { - this.configurationService.get('mon_allow_pool_delete').subscribe((data: any) => { + this.configurationService.get(POOL_CONFIG).subscribe((data: any) => { if (_.has(data, 'value')) { const monSection = _.find(data.value, (v) => { return v.section === 'mon'; }) || { value: false }; - this.monAllowPoolDelete = monSection.value === 'true' ? true : false; + this.monAllowPoolDelete$.next(monSection.value === 'true'); } }); - } else if (this.permissions.pool?.read) { - /* - `monAllowPoolDelete` will always be `false`, - because no read permissions for reading config settings. - Hence enabling by default for pool based roles which allow CRUD. - @TODO: Fix once permissions of config-opt are sorted. - */ - this.monAllowPoolDelete = true; } } @@ -192,9 +193,27 @@ export class PoolListComponent extends ListWithDetails implements OnInit { deletePoolModal() { const name = this.selection.first().pool_name; + if (!this.monAllowPoolDelete$.getValue()) { + if (this.permissions.configOpt.read && this.permissions.pool.read) { + this.modalService.show(ConfirmationModalComponent, { + titleText: $localize`Can't delete ${name}`, + buttonText: $localize`Enable`, + bodyTpl: this.poolEnableTpl, + warning: true, + onSubmit: () => this.enablePoolDeletion() + }); + } else { + this.modalService.show(DeleteGuardModalComponent, { + resourceName: name, + resourceType: $localize`Pool`, + bodyTemplate: this.poolNoPermsTpl + }); + } + return; + } this.modalService.show(DeleteConfirmationModalComponent, { impact: DeletionImpact.high, - itemDescription: 'Pool', + itemDescription: $localize`Pool`, itemNames: [name], submitActionObservable: () => this.taskWrapper.wrapTaskAroundCall({ @@ -204,6 +223,19 @@ export class PoolListComponent extends ListWithDetails implements OnInit { }); } + enablePoolDeletion() { + this.configurationService + .create({ + name: POOL_CONFIG, + value: [{ section: 'mon', value: true }], + force_update: false + }) + .subscribe(() => { + this.monAllowPoolDelete$.next(true); + this.modalService.dismissAll(); + }); + } + getPgStatusCellClass(_row: any, _column: any, value: string): object { return { 'text-right': true, @@ -279,18 +311,6 @@ export class PoolListComponent extends ListWithDetails implements OnInit { } } - getDisableDesc(): boolean | string { - if (this.selection?.hasSelection) { - if (!this.monAllowPoolDelete) { - return $localize`Pool deletion is disabled by the mon_allow_pool_delete configuration setting.`; - } - - return false; - } - - return true; - } - setExpandedRow(expandedRow: any) { super.setExpandedRow(expandedRow); this.getSelectionTiers(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html index fd7f13e12950..6c873775c060 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html @@ -435,7 +435,7 @@ type="info" *ngIf="bucketForm.getValue('lock_enabled')" class="me-1" - i18n-title + i18n-alertTitle > Bucket Versioning can't be disabled when Object Locking is enabled.
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html index e654573ab2e6..1a07480d85e4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html @@ -47,8 +47,8 @@ id="alert-self-test-unknown" size="slim" type="warning" - i18n-title - title="SMART overall-health self-assessment test result" + i18n-alertTitle + alertTitle="SMART overall-health self-assessment test result" i18n >unknown
@@ -59,8 +59,8 @@ id="alert-self-test-passed" size="slim" type="info" - i18n-title - title="SMART overall-health self-assessment test result" + i18n-alertTitle + alertTitle="SMART overall-health self-assessment test result" i18n >passed
@@ -70,8 +70,8 @@ id="alert-self-test-failed" size="slim" type="warning" - i18n-title - title="SMART overall-health self-assessment test result" + i18n-alertTitle + alertTitle="SMART overall-health self-assessment test result" i18n >failed
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts index 4d577d9257a1..ba37f3d5f97f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts @@ -89,31 +89,20 @@ describe('OsdSmartListComponent', () => { component.ngOnChanges(changes); }; - /** - * Verify an alert panel and its attributes. - * - * @param selector The CSS selector for the alert panel. - * @param panelTitle The title should be displayed. - * @param panelType Alert level of panel. Can be in `warning` or `info`. - * @param panelSize Pass `slim` for slim alert panel. - */ const verifyAlertPanel = ( selector: string, panelTitle: string, - panelType: 'warning' | 'info', - panelSize?: 'slim' + panelType: 'warning' | 'info' ) => { const alertPanel = fixture.debugElement.query(By.css(selector)); expect(component.incompatible).toBe(false); expect(component.loading).toBe(false); - expect(alertPanel.attributes.type).toBe(panelType); - if (panelSize === 'slim') { - expect(alertPanel.attributes.title).toBe(panelTitle); - expect(alertPanel.attributes.size).toBe(panelSize); + if (alertPanel.attributes.alertTitle) { + expect(alertPanel.attributes.alertTitle).toBe(panelTitle); } else { const panelText = alertPanel.query(By.css('.cds--actionable-notification__content')); - expect(panelText.nativeElement.textContent).toBe(panelTitle); + expect(panelText.nativeElement.textContent).toContain(panelTitle); } }; @@ -207,8 +196,7 @@ describe('OsdSmartListComponent', () => { verifyAlertPanel( 'cd-alert-panel#alert-self-test-passed', 'SMART overall-health self-assessment test result', - 'info', - 'slim' + 'info' ); }); @@ -218,8 +206,7 @@ describe('OsdSmartListComponent', () => { verifyAlertPanel( 'cd-alert-panel#alert-self-test-failed', 'SMART overall-health self-assessment test result', - 'warning', - 'slim' + 'warning' ); }); @@ -229,8 +216,7 @@ describe('OsdSmartListComponent', () => { verifyAlertPanel( 'cd-alert-panel#alert-self-test-unknown', 'SMART overall-health self-assessment test result', - 'warning', - 'slim' + 'warning' ); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html index bbc1790da0ce..1bc504e54f05 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html @@ -6,6 +6,9 @@ > + @if (showTitle && alertTitle) { + {{ alertTitle }} + } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts index 0e342bd1c6bb..8e95d7cf0b3b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts @@ -24,14 +24,12 @@ export class AlertPanelComponent implements OnInit { actionTpl: TemplateRef; @Input() - title = ''; + alertTitle = ''; @Input() type: 'warning' | 'error' | 'info' | 'success' | 'danger'; @Input() showTitle = true; @Input() - size: 'slim' | 'normal' = 'normal'; - @Input() dismissible = false; @Input() spacingClass = ''; @@ -64,19 +62,19 @@ export class AlertPanelComponent implements OnInit { const type: NotificationType = this.type === 'danger' ? 'error' : this.type; switch (this.type) { case 'warning': - this.title = this.title || $localize`Warning`; + this.alertTitle = this.alertTitle || $localize`Warning`; break; case 'error': - this.title = this.title || $localize`Error`; + this.alertTitle = this.alertTitle || $localize`Error`; break; case 'info': - this.title = this.title || $localize`Information`; + this.alertTitle = this.alertTitle || $localize`Information`; break; case 'success': - this.title = this.title || $localize`Success`; + this.alertTitle = this.alertTitle || $localize`Success`; break; case 'danger': - this.title = this.title || $localize`Danger`; + this.alertTitle = this.alertTitle || $localize`Danger`; break; } @@ -85,7 +83,7 @@ export class AlertPanelComponent implements OnInit { template: this.alertContent, actionsTemplate: this.actionTpl, showClose: this.dismissible, - title: this.showTitle ? this.title : '', + title: this.showTitle ? this.alertTitle : '', lowContrast: this.lowContrast, variant: this.variant }; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts index 273d5d872491..e0cf05e21f37 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -59,6 +59,7 @@ import { ConfigOptionComponent } from './config-option/config-option.component'; import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component'; import { Copy2ClipboardButtonComponent } from './copy2clipboard-button/copy2clipboard-button.component'; import { DeleteConfirmationModalComponent } from './delete-confirmation-modal/delete-confirmation-modal.component'; +import { DeleteGuardModalComponent } from './delete-guard-modal/delete-guard-modal.component'; import { CustomLoginBannerComponent } from './custom-login-banner/custom-login-banner.component'; import { DateTimePickerComponent } from './date-time-picker/date-time-picker.component'; import { DocComponent } from './doc/doc.component'; @@ -200,6 +201,7 @@ import { OverviewComponent } from './resource-overview-card/resource-overview-ca LoadingPanelComponent, ModalComponent, DeleteConfirmationModalComponent, + DeleteGuardModalComponent, ConfirmationModalComponent, LanguageSelectorComponent, GrafanaComponent, 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 3787dd2077f2..537e56761521 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 @@ -75,11 +75,16 @@

@if (bodyContext?.deletionMessage) { + } @else if (hasAssociatedResources) { + + Deleting {{ itemNames[0] }} will remove all associated + {{ itemDescription }}. This action cannot be undone. + } @else { - Deleting {{ itemNames[0] }} will remove all associated - {{ itemDescription }}.This action cannot be undone. + + Deleting {{ itemNames[0] }} permanently deletes the pool. This + action cannot be undone. + }

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 658de582ff27..0467ca5356ae 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 @@ -41,6 +41,7 @@ export class DeleteConfirmationModalComponent extends BaseModal implements OnIni @Optional() @Inject('callBackAtionObservable') public callBackAtionObservable?: () => Observable, + @Optional() @Inject('hasAssociatedResources') public hasAssociatedResources?: boolean, @Optional() @Inject('hideDefaultWarning') public hideDefaultWarning?: boolean, @Optional() @Inject('childFormGroup') public childFormGroup?: CdFormGroup, @Optional() @Inject('childFormGroupTemplate') public childFormGroupTemplate?: TemplateRef @@ -87,10 +88,6 @@ 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]); @@ -153,15 +150,4 @@ 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/components/delete-guard-modal/delete-guard-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-guard-modal/delete-guard-modal.component.html new file mode 100644 index 000000000000..6e6da66736ee --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-guard-modal/delete-guard-modal.component.html @@ -0,0 +1,60 @@ + + +

+ Manage {{ resourceType }} +

+ Can't delete {{ resourceName }} +
+
+ @if (bodyTemplate) { + + } @else { +

+ {{ message }} +

+ @if (connectedItems.length) { +

+ {{ connectedItemsLabel }} +

+
+ @for (item of connectedItems; track item.name) { +
+ @if (item.route?.length) { + + {{ item.name }} + + + } @else { + {{ item.name }} + } +
+ } +
+ } + } +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-guard-modal/delete-guard-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-guard-modal/delete-guard-modal.component.spec.ts new file mode 100644 index 000000000000..22ff2d8aa676 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-guard-modal/delete-guard-modal.component.spec.ts @@ -0,0 +1,90 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ModalModule } from 'carbon-components-angular'; +import { DeleteGuardModalComponent } from './delete-guard-modal.component'; + +describe('DeleteGuardModalComponent', () => { + let component: DeleteGuardModalComponent; + let fixture: ComponentFixture; + let router: Router; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ModalModule], + declarations: [DeleteGuardModalComponent], + providers: [ + { + provide: Router, + useValue: { navigate: jest.fn() } + }, + { provide: 'resourceName', useValue: 'my-pool' }, + { provide: 'resourceType', useValue: 'Pool' } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + fixture = TestBed.createComponent(DeleteGuardModalComponent); + component = fixture.componentInstance; + router = TestBed.inject(Router); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set default values', () => { + expect(component.resourceName).toBe('my-pool'); + expect(component.resourceType).toBe('Pool'); + expect(component.connectedItems).toEqual([]); + expect(component.message).toContain('connected items'); + expect(component.connectedItemsLabel).toContain('View connected items'); + }); + + it('should navigate to item route and close modal', () => { + const closeSpy = jest.spyOn(component, 'closeModal'); + const item = { + name: 'subsystem-1', + route: ['/block/nvmeof/subsystems', 'subsystem-1', 'overview'], + queryParams: { group: 'default' } + }; + + component.navigateToItem(item); + + expect(router.navigate).toHaveBeenCalledWith( + ['/block/nvmeof/subsystems', 'subsystem-1', 'overview'], + { queryParams: { group: 'default' } } + ); + expect(closeSpy).toHaveBeenCalled(); + }); + + it('should not navigate if item has no route', () => { + const closeSpy = jest.spyOn(component, 'closeModal'); + const item = { name: 'no-route-item' }; + + component.navigateToItem(item); + + expect(router.navigate).not.toHaveBeenCalled(); + expect(closeSpy).not.toHaveBeenCalled(); + }); + + it('should use default resourceType when not provided', async () => { + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + imports: [ModalModule], + declarations: [DeleteGuardModalComponent], + providers: [ + { provide: Router, useValue: { navigate: jest.fn() } }, + { provide: 'resourceName', useValue: 'test-resource' } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + const defaultFixture = TestBed.createComponent(DeleteGuardModalComponent); + const defaultComponent = defaultFixture.componentInstance; + defaultFixture.detectChanges(); + + expect(defaultComponent.resourceType).toBe('resource'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-guard-modal/delete-guard-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-guard-modal/delete-guard-modal.component.ts new file mode 100644 index 000000000000..df218a60fc2f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-guard-modal/delete-guard-modal.component.ts @@ -0,0 +1,39 @@ +import { Component, Inject, Optional, TemplateRef } from '@angular/core'; +import { Router } from '@angular/router'; +import { BaseModal } from 'carbon-components-angular'; +import { ConnectedItem } from '../../models/delete-guard.model'; + +@Component({ + selector: 'cd-delete-guard-modal', + templateUrl: './delete-guard-modal.component.html', + standalone: false +}) +export class DeleteGuardModalComponent extends BaseModal { + constructor( + private router: Router, + @Optional() @Inject('resourceName') public resourceName: string, + @Optional() @Inject('resourceType') public resourceType: string, + @Optional() @Inject('connectedItems') public connectedItems: ConnectedItem[], + @Optional() @Inject('message') public message: string, + @Optional() @Inject('connectedItemsLabel') public connectedItemsLabel: string, + @Optional() @Inject('bodyTemplate') public bodyTemplate: TemplateRef, + @Optional() @Inject('bodyContext') public bodyContext: any + ) { + super(); + this.resourceType = this.resourceType || $localize`resource`; + this.connectedItems = this.connectedItems || []; + this.message = + this.message || + $localize`This resource has connected items that must be deleted first. Delete the connected items, and try again.`; + this.connectedItemsLabel = this.connectedItemsLabel || $localize`View connected items:`; + } + + navigateToItem(item: ConnectedItem): void { + if (item.route?.length) { + this.router.navigate(item.route, { + queryParams: item.queryParams + }); + this.closeModal(); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts index 9ab8d33a51b5..07a355463d7d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts @@ -61,12 +61,13 @@ describe('FormLoadingDirective', () => { component.loadingError(); fixture.detectChanges(); - expectShown(0, 1, 0); + expect(fixture.debugElement.queryAll(By.css('cd-alert-panel')).length).toEqual(1); + expect(fixture.debugElement.queryAll(By.css('cd-loading-panel')).length).toEqual(0); const alert = fixture.debugElement.nativeElement.querySelector( 'cd-alert-panel .cds--actionable-notification__content' ); - expect(alert.textContent).toBe('Form data could not be loaded.'); + expect(alert.textContent).toContain('Form data could not be loaded.'); }); it('should show original component when calling loadingReady()', () => { 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 index 7f4af3123ff9..9bea38cea2df 100644 --- 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 @@ -1,6 +1,5 @@ export interface DeleteConfirmationBodyContext { warningMessage?: string; - disableForm?: boolean; inputLabel?: string; inputPlaceholder?: string; deletionMessage?: string; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-guard.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-guard.model.ts new file mode 100644 index 000000000000..2896e92399fb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-guard.model.ts @@ -0,0 +1,5 @@ +export interface ConnectedItem { + name: string; + route?: string[]; + queryParams?: Record; +}