mirror of
https://github.com/ceph/ceph
synced 2026-08-02 07:03:18 +00:00
Merge pull request #70643 from rhcs-dashboard/fix-pool-deletion
mgr/dashboard: Pool deletion enhancements Reviewed-by: Afreen Misbah <afreen@ibm.com> Reviewed-by: Naman Munet <nmunet@redhat.com>
This commit is contained in:
commit
1066090245
@ -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;
|
||||
|
||||
@ -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
|
||||
],
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
<cd-alert-panel
|
||||
type="info"
|
||||
*ngIf="available === false"
|
||||
title="iSCSI Targets not available"
|
||||
i18n-title
|
||||
alertTitle="iSCSI Targets not available"
|
||||
i18n-alertTitle
|
||||
>
|
||||
<ng-container i18n
|
||||
>Please consult the <cd-doc section="iscsi"></cd-doc> on how to configure and enable
|
||||
|
||||
@ -13,8 +13,8 @@
|
||||
@if (showAuthAlert) {
|
||||
<cd-alert-panel
|
||||
type="warning"
|
||||
title="Subsystem DHCHAP Key will be deleted permanently"
|
||||
i18n-title
|
||||
alertTitle="Subsystem DHCHAP Key will be deleted permanently"
|
||||
i18n-alertTitle
|
||||
class="cd-nvmeof-edit-auth-downgrade-alert"
|
||||
>
|
||||
<span i18n>
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
<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"
|
||||
tabindex="0"
|
||||
(click)="navigateToSubsystem(sub.nqn)"
|
||||
(keydown.enter)="navigateToSubsystem(sub.nqn)"
|
||||
>
|
||||
<span class="cds--mr-03">{{ sub.nqn }}</span>
|
||||
<cd-icon type="launch"></cd-icon>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</cds-modal>
|
||||
@ -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<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();
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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' }
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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 <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.`
|
||||
},
|
||||
hasAssociatedResources: true,
|
||||
submitActionObservable: () => {
|
||||
return this.taskWrapper
|
||||
.wrapTaskAroundCall({
|
||||
|
||||
@ -456,11 +456,7 @@ describe('NvmeofGatewayNodeComponent', () => {
|
||||
actionDescription: 'remove',
|
||||
hideDefaultWarning: true,
|
||||
impact: DeletionImpact.high,
|
||||
|
||||
bodyContext: {
|
||||
deletionMessage:
|
||||
'Removing <strong>host1</strong> will detach it from the gateway group and stop handling new I/O requests. Active connections may be disrupted.<br><br>You can re-add this node later if required.'
|
||||
},
|
||||
hasAssociatedResources: true,
|
||||
submitActionObservable: jasmine.any(Function)
|
||||
});
|
||||
|
||||
|
||||
@ -224,9 +224,7 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges
|
||||
actionDescription: $localize`remove`,
|
||||
hideDefaultWarning: true,
|
||||
impact: DeletionImpact.high,
|
||||
bodyContext: {
|
||||
deletionMessage: $localize`Removing <strong>${hostname}</strong> will detach it from the gateway group and stop handling new I/O requests. Active connections may be disrupted.<br><br>You can re-add this node later if required.`
|
||||
},
|
||||
hasAssociatedResources: true,
|
||||
submitActionObservable: () => {
|
||||
const updatedSpec = this.buildRemoveGatewaySpecPayload(hostname);
|
||||
if (!updatedSpec) {
|
||||
|
||||
@ -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 <strong>${hostName}</strong> will disconnect it and revoke its permissions for the <strong>${this.subsystemNQN}</strong> subsystem.`
|
||||
},
|
||||
hasAssociatedResources: true,
|
||||
submitActionObservable: () =>
|
||||
this.taskWrapper.wrapTaskAroundCall({
|
||||
task: new FinishedTask('nvmeof/initiator/remove', {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<cd-alert-panel
|
||||
*ngIf="listeners && listeners.length === 0"
|
||||
type="error"
|
||||
title="No listeners exists"
|
||||
alertTitle="No listeners exists"
|
||||
spacingClass="cds-mb-3"
|
||||
i18n-title
|
||||
i18n-alertTitle
|
||||
>
|
||||
<ng-container i18n
|
||||
>Currently, there are no listeners available in the NVMe subsystem. Please check your
|
||||
|
||||
@ -264,9 +264,7 @@ export class NvmeofNamespacesListComponent implements OnInit, OnDestroy {
|
||||
bodyTemplate: this.deleteTpl,
|
||||
itemNames: [namespace.nsid],
|
||||
actionDescription: 'delete',
|
||||
bodyContext: {
|
||||
deletionMessage: $localize`Deleting the namespace <strong>${namespace.nsid}</strong> will permanently remove all resources, services, and configurations within it. This action cannot be undone.`
|
||||
},
|
||||
hasAssociatedResources: true,
|
||||
submitActionObservable: () =>
|
||||
this.taskWrapper
|
||||
.wrapTaskAroundCall({
|
||||
|
||||
@ -52,11 +52,11 @@
|
||||
>
|
||||
@if (formGroup.get('hostType').value === HOST_TYPE.ALL) {
|
||||
<cd-alert-panel
|
||||
title="Caution"
|
||||
alertTitle="Caution"
|
||||
type="warning"
|
||||
class="cds-mb-3"
|
||||
i18n
|
||||
i18n-title
|
||||
i18n-alertTitle
|
||||
>
|
||||
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.
|
||||
|
||||
@ -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 <strong>${subsystem.nqn}</strong> 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: () =>
|
||||
|
||||
@ -291,8 +291,8 @@
|
||||
<ng-template #daemonLogsTpl>
|
||||
<cd-alert-panel
|
||||
type="info"
|
||||
title="Loki/Alloy service not running"
|
||||
i18n-title
|
||||
alertTitle="Loki/Alloy service not running"
|
||||
i18n-alertTitle
|
||||
>
|
||||
<ng-container i18n>Please start the loki and alloy services to see these logs.</ng-container>
|
||||
</cd-alert-panel>
|
||||
|
||||
@ -84,3 +84,22 @@
|
||||
{{ row.pool_name }}
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #poolEnableTpl>
|
||||
<p
|
||||
class="cds--type-body-01 cds--mb-05"
|
||||
i18n
|
||||
>
|
||||
To delete this pool, first enable pool deletion.
|
||||
</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #poolNoPermsTpl>
|
||||
<p
|
||||
class="cds--type-body-01 cds--mb-05"
|
||||
i18n
|
||||
>
|
||||
Pool deletion is currently disabled.<br />
|
||||
You do not have permissions to enable. Please contact administrator.
|
||||
</p>
|
||||
</ng-template>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<any>;
|
||||
|
||||
@ViewChild('poolEnableTpl', { static: true })
|
||||
poolEnableTpl!: TemplateRef<any>;
|
||||
|
||||
@ViewChild('poolNoPermsTpl', { static: true })
|
||||
poolNoPermsTpl!: TemplateRef<any>;
|
||||
|
||||
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<boolean>(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();
|
||||
|
||||
@ -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.
|
||||
</cd-alert-panel>
|
||||
|
||||
@ -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</cd-alert-panel
|
||||
>
|
||||
@ -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</cd-alert-panel
|
||||
>
|
||||
@ -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</cd-alert-panel
|
||||
>
|
||||
|
||||
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -6,6 +6,9 @@
|
||||
></cds-actionable-notification>
|
||||
|
||||
<ng-template #content>
|
||||
@if (showTitle && alertTitle) {
|
||||
<span class="cds--actionable-notification__title">{{ alertTitle }}</span>
|
||||
}
|
||||
<ng-content></ng-content>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@ -24,14 +24,12 @@ export class AlertPanelComponent implements OnInit {
|
||||
actionTpl: TemplateRef<any>;
|
||||
|
||||
@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
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -75,11 +75,16 @@
|
||||
<p class="cds--type-body-01">
|
||||
@if (bodyContext?.deletionMessage) {
|
||||
<span [innerHTML]="bodyContext.deletionMessage"></span>
|
||||
} @else if (hasAssociatedResources) {
|
||||
<ng-container i18n>
|
||||
Deleting <strong>{{ itemNames[0] }}</strong> will remove all associated
|
||||
{{ itemDescription }}. This action cannot be undone.
|
||||
</ng-container>
|
||||
} @else {
|
||||
<ng-container i18n
|
||||
>Deleting <strong>{{ itemNames[0] }}</strong> will remove all associated
|
||||
{{ itemDescription }}.This action cannot be undone.</ng-container
|
||||
>
|
||||
<ng-container i18n>
|
||||
Deleting <strong>{{ itemNames[0] }}</strong> permanently deletes the pool. This
|
||||
action cannot be undone.
|
||||
</ng-container>
|
||||
}
|
||||
</p>
|
||||
<ng-template #labelTemplate>
|
||||
|
||||
@ -41,6 +41,7 @@ export class DeleteConfirmationModalComponent extends BaseModal implements OnIni
|
||||
@Optional()
|
||||
@Inject('callBackAtionObservable')
|
||||
public callBackAtionObservable?: () => Observable<any>,
|
||||
@Optional() @Inject('hasAssociatedResources') public hasAssociatedResources?: boolean,
|
||||
@Optional() @Inject('hideDefaultWarning') public hideDefaultWarning?: boolean,
|
||||
@Optional() @Inject('childFormGroup') public childFormGroup?: CdFormGroup,
|
||||
@Optional() @Inject('childFormGroupTemplate') public childFormGroupTemplate?: TemplateRef<any>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
<cds-modal
|
||||
size="sm"
|
||||
[open]="open"
|
||||
(overlaySelected)="closeModal()"
|
||||
>
|
||||
<cds-modal-header (closeSelect)="closeModal()">
|
||||
<h2
|
||||
cdsModalHeaderLabel
|
||||
modal-primary-focus
|
||||
i18n
|
||||
>
|
||||
Manage {{ resourceType }}
|
||||
</h2>
|
||||
<span
|
||||
class="cds--type-heading-03"
|
||||
cdsModalHeaderHeading
|
||||
i18n
|
||||
>Can't delete {{ resourceName }}</span
|
||||
>
|
||||
</cds-modal-header>
|
||||
<section cdsModalContent>
|
||||
@if (bodyTemplate) {
|
||||
<ng-container *ngTemplateOutlet="bodyTemplate; context: bodyContext"></ng-container>
|
||||
} @else {
|
||||
<p
|
||||
class="cds--type-body-01 cds--mb-05"
|
||||
i18n
|
||||
>
|
||||
{{ message }}
|
||||
</p>
|
||||
@if (connectedItems.length) {
|
||||
<p
|
||||
class="cds--type-label-01 cds--mb-04"
|
||||
i18n
|
||||
>
|
||||
{{ connectedItemsLabel }}
|
||||
</p>
|
||||
<div>
|
||||
@for (item of connectedItems; track item.name) {
|
||||
<div class="cds--mb-04">
|
||||
@if (item.route?.length) {
|
||||
<a
|
||||
class="cds--link cds--type-body-01"
|
||||
tabindex="0"
|
||||
(click)="navigateToItem(item)"
|
||||
(keydown.enter)="navigateToItem(item)"
|
||||
>
|
||||
<span class="cds--mr-03">{{ item.name }}</span>
|
||||
<cd-icon type="launch"></cd-icon>
|
||||
</a>
|
||||
} @else {
|
||||
<span class="cds--type-body-01">{{ item.name }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
</cds-modal>
|
||||
@ -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<DeleteGuardModalComponent>;
|
||||
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');
|
||||
});
|
||||
});
|
||||
@ -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<any>,
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()', () => {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
export interface DeleteConfirmationBodyContext {
|
||||
warningMessage?: string;
|
||||
disableForm?: boolean;
|
||||
inputLabel?: string;
|
||||
inputPlaceholder?: string;
|
||||
deletionMessage?: string;
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
export interface ConnectedItem {
|
||||
name: string;
|
||||
route?: string[];
|
||||
queryParams?: Record<string, string>;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user