.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;
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';
NvmeofSubsystemOverviewComponent,
NvmeofSubsystemPerformanceComponent,
NvmeofTabsComponent,
- NvmeofGatewayGroupDeleteGuardModalComponent,
NvmeofEditAuthenticationComponent
],
<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
@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>
+++ /dev/null
-<cds-modal
- size="sm"
- [open]="open"
- (overlaySelected)="closeModal()"
->
- <cds-modal-header (closeSelect)="closeModal()">
- <h2
- cdsModalHeaderLabel
- modal-primary-focus
- i18n
- >
- Manage {{ gatewayName }}
- </h2>
- <span
- class="cds--type-heading-03"
- cdsModalHeaderHeading
- i18n
- >Can't delete {{ gatewayName }}</span
- >
- </cds-modal-header>
- <section cdsModalContent>
- <p
- class="cds--type-body-01 cds--mb-05"
- i18n
- >
- This resource has connected items that must be deleted first. Delete the connected items, and
- try again.
- </p>
- <p
- class="cds--type-label-01 cds--mb-04"
- i18n
- >
- View connected items:
- </p>
- <div>
- @for (sub of connectedSubsystems; track sub.nqn) {
- <div class="cds--mb-04">
- <a
- class="cds--link cds--type-body-01"
- 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>
+++ /dev/null
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { Router } from '@angular/router';
-import { RouterTestingModule } from '@angular/router/testing';
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { NvmeofGatewayGroupDeleteGuardModalComponent } from './nvmeof-gateway-group-delete-guard-modal.component';
-
-describe('NvmeofGatewayGroupDeleteGuardModalComponent', () => {
- let component: NvmeofGatewayGroupDeleteGuardModalComponent;
- let fixture: ComponentFixture<NvmeofGatewayGroupDeleteGuardModalComponent>;
- let router: Router;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [RouterTestingModule],
- declarations: [NvmeofGatewayGroupDeleteGuardModalComponent],
- providers: [
- { provide: 'gatewayName', useValue: 'gateway-dev' },
- {
- provide: 'connectedSubsystems',
- useValue: [{ nqn: 'subsystem-1' }, { nqn: 'subsystem-2' }]
- }
- ],
- schemas: [NO_ERRORS_SCHEMA]
- }).compileComponents();
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(NvmeofGatewayGroupDeleteGuardModalComponent);
- component = fixture.componentInstance;
- router = TestBed.inject(Router);
- jest.spyOn(router, 'navigate').mockImplementation();
- fixture.detectChanges();
- });
-
- it('should create the modal component', () => {
- expect(component).toBeTruthy();
- });
-
- it('should load dynamic inputs', () => {
- expect(component.gatewayName).toBe('gateway-dev');
- expect(component.connectedSubsystems).toEqual([{ nqn: 'subsystem-1' }, { nqn: 'subsystem-2' }]);
- });
-
- it('should navigate to subsystem detail page and close modal', () => {
- const closeSpy = jest.spyOn(component, 'closeModal').mockImplementation();
-
- component.navigateToSubsystem('subsystem-1');
-
- expect(router.navigate).toHaveBeenCalledWith(
- ['/block/nvmeof/subsystems', 'subsystem-1', 'overview'],
- { queryParams: { group: 'gateway-dev' } }
- );
- expect(closeSpy).toHaveBeenCalled();
- });
-});
+++ /dev/null
-import { Component, Inject, Optional } from '@angular/core';
-import { Router } from '@angular/router';
-import { BaseModal } from 'carbon-components-angular';
-
-interface ConnectedSubsystem {
- nqn: string;
-}
-
-@Component({
- selector: 'cd-nvmeof-gateway-group-delete-guard-modal',
- templateUrl: './nvmeof-gateway-group-delete-guard-modal.component.html',
- standalone: false
-})
-export class NvmeofGatewayGroupDeleteGuardModalComponent extends BaseModal {
- constructor(
- private router: Router,
- @Optional() @Inject('gatewayName') public gatewayName: string,
- @Optional() @Inject('connectedSubsystems') public connectedSubsystems: ConnectedSubsystem[] = []
- ) {
- super();
- }
-
- navigateToSubsystem(nqn: string): void {
- this.router.navigate(['/block/nvmeof/subsystems', nqn, 'overview'], {
- queryParams: { group: this.gatewayName }
- });
- this.closeModal();
- }
-}
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';
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' }
+ }
+ ]
});
});
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';
}
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 {
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({
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)
});
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) {
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', {
<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
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({
>
@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.
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: () =>
<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>
{{ 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>
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';
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', () => {
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', () => {
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);
});
});
});
});
- 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);
});
});
});
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';
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';
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;
@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();
tableActions: CdTableAction[] = [];
tableStatus = new TableStatusViewCache();
cacheTiers: any[] = [];
- monAllowPoolDelete = false;
+ monAllowPoolDelete$ = new BehaviorSubject<boolean>(false);
ecProfileList: ErasureCodeProfile[] = [];
viewUrl = '/pool/view';
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;
}
}
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({
});
}
+ 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,
}
}
- 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();
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>
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
>
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
>
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
>
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);
}
};
verifyAlertPanel(
'cd-alert-panel#alert-self-test-passed',
'SMART overall-health self-assessment test result',
- 'info',
- 'slim'
+ 'info'
);
});
verifyAlertPanel(
'cd-alert-panel#alert-self-test-failed',
'SMART overall-health self-assessment test result',
- 'warning',
- 'slim'
+ 'warning'
);
});
verifyAlertPanel(
'cd-alert-panel#alert-self-test-unknown',
'SMART overall-health self-assessment test result',
- 'warning',
- 'slim'
+ 'warning'
);
});
></cds-actionable-notification>
<ng-template #content>
+ @if (showTitle && alertTitle) {
+ <span class="cds--actionable-notification__title">{{ alertTitle }}</span>
+ }
<ng-content></ng-content>
</ng-template>
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 = '';
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;
}
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
};
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';
LoadingPanelComponent,
ModalComponent,
DeleteConfirmationModalComponent,
+ DeleteGuardModalComponent,
ConfirmationModalComponent,
LanguageSelectorComponent,
GrafanaComponent,
<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>
@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>
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]);
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);
- }
- }
}
--- /dev/null
+<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>
--- /dev/null
+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');
+ });
+});
--- /dev/null
+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();
+ }
+ }
+}
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()', () => {
export interface DeleteConfirmationBodyContext {
warningMessage?: string;
- disableForm?: boolean;
inputLabel?: string;
inputPlaceholder?: string;
deletionMessage?: string;
--- /dev/null
+export interface ConnectedItem {
+ name: string;
+ route?: string[];
+ queryParams?: Record<string, string>;
+}