From: Naman Munet Date: Wed, 22 Jan 2025 10:59:20 +0000 (+0530) Subject: mgr/dashboard: Add confirmation textbox for resource name on delete action X-Git-Tag: testing/wip-pdonnell-testing-20250218.200041-debug~7^2~1 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=ed222df10900198707d7708518e02af5c06c912b;p=ceph-ci.git mgr/dashboard: Add confirmation textbox for resource name on delete action Before: ===== User was able to delete a single or multiple critical resources like ( images, snapshots, subvolumes, subvolume-groups, pools, hosts , OSDs, buckets, file system, services ) by just clicking on a checkbox. After: ===== User now has to type the resource name that they are deleting in the textbox on the delete modal, and then only they will be able to delete the critical resource. Also from now onwards multiple selection for deletions of critical resources is not possible. Hence, user can delete only single resource at a time. On the other side, non-critical resources can be deleted in one go. fixes: https://tracker.ceph.com/issues/69628 Signed-off-by: Naman Munet --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts index 847a75b630a..ef075f0f4b6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts @@ -16,6 +16,7 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { PoolEditPeerModalComponent } from '../pool-edit-peer-modal/pool-edit-peer-modal.component'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = '/block/mirroring'; @Component({ @@ -147,6 +148,7 @@ export class PoolListComponent implements OnInit, OnDestroy { const peerUUID = this.getPeerUUID(); this.modalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: $localize`mirror peer`, itemNames: [`${poolName} (${peerUUID})`], submitActionObservable: () => diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts index 52d9ff819e2..9e5c4fc9e3f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts @@ -32,6 +32,7 @@ import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-mo import { RBDImageFormat, RbdModel } from './rbd-model'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { RBDActionHelpers } from '../rbd-contants'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = 'block/rbd'; @Component({ @@ -426,8 +427,9 @@ export class RbdListComponent extends ListWithDetails implements OnInit { const imageSpec = new ImageSpec(poolName, namespace, imageName); this.cdsModalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: 'RBD', - itemNames: [imageSpec], + itemNames: [imageSpec.imageName], bodyTemplate: this.deleteTpl, bodyContext: { hasSnapshots: this.hasSnapshots(), diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts index d014598214d..ac20dc76fa3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts @@ -37,6 +37,7 @@ import { RbdSnapshotFormModalComponent } from '../rbd-snapshot-form/rbd-snapshot import { RbdSnapshotActionsModel } from './rbd-snapshot-actions.model'; import { RbdSnapshotModel } from './rbd-snapshot.model'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-rbd-snapshot-list', @@ -325,6 +326,7 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { deleteSnapshotModal() { const snapshotName = this.selection.selected[0].name; this.modalRef = this.cdsModalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: $localize`RBD snapshot`, itemNames: [snapshotName], submitAction: () => this._asyncTask('deleteSnapshot', 'rbd/snap/delete', snapshotName) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.html index a6a64bf2734..a1a74471b37 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.html @@ -53,7 +53,7 @@ [columns]="snapshot.columns" identifier="name" forceIdentifier="true" - selectionType="multiClick" + selectionType="single" (updateSelection)="snapshot.updateSelection($event)"> this.deleteSnapshotModal(), - canBePrimary: (selection) => selection.hasSelection, - disable: (selection) => !selection.hasSelection + click: () => this.deleteSnapshotModal() } ] }; @@ -684,6 +683,7 @@ export class CephfsDirectoriesComponent implements OnInit, OnChanges { deleteSnapshotModal() { this.modalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: $localize`CephFs Snapshot`, itemNames: this.snapshot.selection.selected.map((snapshot: CephfsSnapshot) => snapshot.name), submitAction: () => this.deleteSnapshot() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts index 41c1ff76fe4..cda087b0a33 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts @@ -26,6 +26,7 @@ import { map, switchMap } from 'rxjs/operators'; import { HealthService } from '~/app/shared/api/health.service'; import { CephfsAuthModalComponent } from '~/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = 'cephfs/fs'; @@ -173,6 +174,7 @@ export class CephfsListComponent extends ListWithDetails implements OnInit { removeVolumeModal() { const volName = this.selection.first().mdsmap['fs_name']; this.cdsModalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: 'File System', itemNames: [volName], actionDescription: 'remove', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts index ea3a18a5016..167ea4b8c5c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts @@ -20,6 +20,7 @@ import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-cephfs-subvolume-group', @@ -175,6 +176,7 @@ export class CephfsSubvolumeGroupComponent implements OnInit, OnChanges { removeSubVolumeModal() { const name = this.selection.first().name; this.cdsModalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: 'subvolume group', itemNames: [name], actionDescription: 'remove', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts index be7b81940df..013288fc36b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts @@ -35,6 +35,7 @@ import { HealthService } from '~/app/shared/api/health.service'; import _ from 'lodash'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { DEFAULT_SUBVOLUME_GROUP } from '~/app/shared/constants/cephfs.constant'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-cephfs-subvolume-list', @@ -246,7 +247,8 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh this.errorMessage = ''; this.selectedName = this.selection.first().name; this.modalService.show(CriticalConfirmationModalComponent, { - actionDescription: 'Remove', + impact: DeletionImpact.high, + actionDescription: 'remove', itemNames: [this.selectedName], itemDescription: 'Subvolume', childFormGroup: this.removeForm, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts index 0087ffd66cd..770fbf2fe52 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts @@ -27,6 +27,7 @@ import { Validators } from '@angular/forms'; import { CdValidators } from '~/app/shared/forms/cd-validators'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { DEFAULT_SUBVOLUME_GROUP } from '~/app/shared/constants/cephfs.constant'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-cephfs-subvolume-snapshots-list', @@ -228,7 +229,8 @@ export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges const subVolumeGroupName = this.activeGroupName; const fsName = this.fsName; this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { - actionDescription: this.actionLabels.REMOVE, + impact: DeletionImpact.high, + actionDescription: 'remove', itemNames: [snapshotName], itemDescription: 'Snapshot', submitAction: () => diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts index c26d24177fd..702c90e3c5c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts @@ -34,6 +34,7 @@ import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { URLBuilderService } from '~/app/shared/services/url-builder.service'; import { HostFormComponent } from './host-form/host-form.component'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = 'hosts'; @@ -470,6 +471,7 @@ export class HostsComponent extends ListWithDetails implements OnDestroy, OnInit deleteAction() { const hostname = this.selection.first().hostname; this.modalRef = this.cdsModalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: 'Host', itemNames: [hostname], actionDescription: 'remove', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html index a56877512f9..122084410f3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html @@ -8,7 +8,7 @@ ): void { check(this.getSelectedOsdIds()).subscribe((result) => { + const osdIds = this.getSelectedOsdIds(); this.cdsModalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, + itemNames: osdIds, actionDescription: actionDescription, itemDescription: itemDescription, bodyTemplate: this.criticalConfirmationTpl, @@ -592,7 +596,7 @@ export class OsdListComponent extends ListWithDetails implements OnInit { missingStats: result.missing_stats, storedPgs: result.stored_pgs, actionDescription: templateItemDescription, - osdIds: this.getSelectedOsdIds() + osdIds }, childFormGroup: childFormGroup, childFormGroupTemplate: childFormGroupTemplate, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts index a07dcfcdd35..000cda887ef 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts @@ -30,6 +30,7 @@ import { ServiceFormComponent } from './service-form/service-form.component'; import { SettingsService } from '~/app/shared/api/settings.service'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = 'services'; @@ -276,6 +277,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI deleteAction() { const service = this.selection.first(); this.cdsModalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: $localize`Service`, itemNames: [service.service_name], actionDescription: 'delete', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts index 29488ed8613..d6dc5007e21 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts @@ -25,6 +25,7 @@ import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { getFsalFromRoute, getPathfromFsal } from '../utils'; import { SUPPORTED_FSAL } from '../models/nfs.fsal'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; export enum RgwExportType { BUCKET = 'bucket', @@ -212,6 +213,7 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr const export_id = this.selection.first().export_id; this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: $localize`NFS export`, itemNames: [`${cluster_id}:${export_id}`], submitActionObservable: () => 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 19e875f5998..dddfbdd6385 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 @@ -30,6 +30,7 @@ import { URLBuilderService } from '~/app/shared/services/url-builder.service'; import { Pool } from '../pool'; import { PoolStat, PoolStats } from '../pool-stat'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = 'pool'; @@ -224,6 +225,7 @@ export class PoolListComponent extends ListWithDetails implements OnInit { deletePoolModal() { const name = this.selection.first().pool_name; this.modalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: 'Pool', itemNames: [name], submitActionObservable: () => diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html index 8012547a781..07feaca2624 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html @@ -3,7 +3,7 @@ [data]="buckets" [columns]="columns" columnMode="flex" - selectionType="multiClick" + selectionType="single" [hasDetails]="true" (setExpandedRow)="setExpandedRow($event)" (updateSelection)="updateSelection($event)" diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts index b7400219120..b2fa1b4a5e5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts @@ -23,6 +23,7 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { URLBuilderService } from '~/app/shared/services/url-builder.service'; import { Bucket } from '../models/rgw-bucket'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = 'rgw/bucket'; @@ -126,9 +127,7 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit, O permission: 'delete', icon: Icons.destroy, click: () => this.deleteAction(), - disable: () => !this.selection.hasSelection, - name: this.actionLabels.DELETE, - canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection + name: this.actionLabels.DELETE }; this.tableActions = [addAction, editAction, deleteAction]; this.setTableRefreshTimeout(); @@ -156,7 +155,8 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit, O deleteAction() { const itemNames = this.selection.selected.map((bucket: any) => bucket['bid']); this.modalService.show(CriticalConfirmationModalComponent, { - itemDescription: this.selection.hasSingleSelection ? $localize`bucket` : $localize`buckets`, + itemDescription: $localize`bucket`, + impact: DeletionImpact.high, itemNames: itemNames, bodyTemplate: this.deleteTpl, submitActionObservable: () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html index 8c1954a37ba..0bb57267262 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html @@ -5,7 +5,7 @@ [data]="users" [columns]="columns" columnMode="flex" - selectionType="multiClick" + selectionType="single" [hasDetails]="true" (setExpandedRow)="setExpandedRow($event)" (updateSelection)="updateSelection($event)" diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts index 43704df333c..d7d74695864 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts @@ -8,6 +8,7 @@ import { CriticalConfirmationModalComponent } from '~/app/shared/components/crit import { ActionLabelsI18n } 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/critical-confirmation-modal-impact.enum'; import { Icons } from '~/app/shared/enum/icons.enum'; import { CdTableAction } from '~/app/shared/models/cd-table-action'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; @@ -124,8 +125,7 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit { icon: Icons.destroy, click: () => this.deleteAction(), disable: () => !this.selection.hasSelection, - name: this.actionLabels.DELETE, - canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection + name: this.actionLabels.DELETE }; this.tableActions = [addAction, editAction, deleteAction]; this.setTableRefreshTimeout(); @@ -149,6 +149,7 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit { deleteAction() { this.modalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: this.selection.hasSingleSelection ? $localize`user` : $localize`users`, itemNames: this.selection.selected.map((user: any) => user['uid']), submitActionObservable: (): Observable => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts index 7d5c6a5ee77..a414b6a154a 100755 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts @@ -7,6 +7,7 @@ import { UserService } from '~/app/shared/api/user.service'; import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; import { Icons } from '~/app/shared/enum/icons.enum'; import { NotificationType } from '~/app/shared/enum/notification-type.enum'; import { CdTableAction } from '~/app/shared/models/cd-table-action'; @@ -174,6 +175,7 @@ export class UserListComponent implements OnInit { } this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, itemDescription: 'User', itemNames: [username], submitAction: () => this.deleteUser(username) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html index 90e3a1d2be8..3ae4b339da3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html @@ -5,11 +5,11 @@ -
-
+
+
- Yes, I am sure. + + Yes, I am sure. + + + Resource Name + + + + + This field is required. + + + Enter the correct resource name. + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts index 2e0706786e3..814f18a5dd2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts @@ -11,6 +11,7 @@ import { LoadingPanelComponent } from '../loading-panel/loading-panel.component' import { CriticalConfirmationModalComponent } from './critical-confirmation-modal.component'; import { CheckboxModule, ModalService, PlaceholderService } from 'carbon-components-angular'; import { ModalCdsService } from '../../services/modal-cds.service'; +import { DeletionImpact } from '../../enum/critical-confirmation-modal-impact.enum'; @NgModule({}) export class MockModule {} @@ -143,8 +144,8 @@ describe('CriticalConfirmationModalComponent', () => { }); describe('component functions', () => { - const changeValue = (value: boolean) => { - const ctrl = component.deletionForm.get('confirmation'); + const changeValue = (formControl: string, value: any) => { + const ctrl = component.deletionForm.get(formControl); ctrl.setValue(value); ctrl.markAsDirty(); ctrl.updateValueAndValidity(); @@ -167,16 +168,55 @@ describe('CriticalConfirmationModalComponent', () => { beforeEach(() => { component.deletionForm.reset(); + const ctrl = component.deletionForm.get('impact'); + ctrl.setValue(DeletionImpact.normal); + ctrl.markAsDirty(); + ctrl.updateValueAndValidity(); + component.deletionForm.get('confirmation').updateValueAndValidity(); }); it('should test empty values', () => { - component.deletionForm.reset(); testValidation(true, 'required', true); + changeValue('confirmation', true); + changeValue('confirmation', false); + testValidation(true, 'required', true); + }); + }); + + describe('validate confirmInput', () => { + const testValidation = (submitted: boolean, error: string, expected: boolean) => { + expect( + component.deletionForm.showError('confirmInput', { submitted: submitted }, error) + ).toBe(expected); + }; + + beforeEach(() => { component.deletionForm.reset(); - changeValue(true); - changeValue(false); + const ctrl = component.deletionForm.get('impact'); + ctrl.setValue(DeletionImpact.high); + ctrl.markAsDirty(); + ctrl.updateValueAndValidity(); + component.deletionForm.get('confirmInput').updateValueAndValidity(); + }); + + it('should test empty values', () => { + testValidation(true, 'required', true); + changeValue('confirmInput', 'dummytext'); + changeValue('confirmInput', ''); testValidation(true, 'required', true); }); + + it('should give error, if entered resource name is wrong', () => { + component.itemNames = ['resource1']; + changeValue('confirmInput', 'dummytext'); + testValidation(true, 'matchResource', true); + }); + + it('should give error, if entered resource name is correct', () => { + component.itemNames = ['resource1']; + changeValue('confirmInput', 'resource1'); + testValidation(true, 'matchResource', false); + }); }); describe('deletion call', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts index 7230c7707bd..5dc45f7fc8b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts @@ -1,11 +1,12 @@ import { Component, Inject, OnInit, Optional, TemplateRef, ViewChild } from '@angular/core'; -import { UntypedFormControl, Validators } from '@angular/forms'; - +import { UntypedFormControl, AbstractControl, ValidationErrors, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { SubmitButtonComponent } from '../submit-button/submit-button.component'; import { BaseModal } from 'carbon-components-angular'; +import { CdValidators } from '../../forms/cd-validators'; +import { DeletionImpact } from '../../enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-deletion-modal', @@ -16,11 +17,12 @@ export class CriticalConfirmationModalComponent extends BaseModal implements OnI @ViewChild(SubmitButtonComponent, { static: true }) submitButton: SubmitButtonComponent; deletionForm: CdFormGroup; - + impactEnum = DeletionImpact; childFormGroup: CdFormGroup; childFormGroupTemplate: TemplateRef; constructor( + @Optional() @Inject('impact') public impact: DeletionImpact, @Optional() @Inject('itemDescription') public itemDescription: 'entry', @Optional() @Inject('itemNames') public itemNames: string[], @Optional() @Inject('actionDescription') public actionDescription = 'delete', @@ -38,11 +40,28 @@ export class CriticalConfirmationModalComponent extends BaseModal implements OnI ) { super(); this.actionDescription = actionDescription || 'delete'; + this.impact = this.impact || DeletionImpact.normal; } ngOnInit() { const controls = { - confirmation: new UntypedFormControl(false, [Validators.requiredTrue]) + impact: new UntypedFormControl(this.impact), + confirmation: new UntypedFormControl(false, { + validators: [ + CdValidators.composeIf( + { + impact: DeletionImpact.normal + }, + [Validators.requiredTrue] + ) + ] + }), + confirmInput: new UntypedFormControl('', [ + CdValidators.composeIf({ impact: this.impactEnum.high }, [ + this.matchResourceName.bind(this), + Validators.required + ]) + ]) }; if (this.childFormGroup) { controls['child'] = this.childFormGroup; @@ -53,6 +72,13 @@ export class CriticalConfirmationModalComponent extends BaseModal implements OnI } } + matchResourceName(control: AbstractControl): ValidationErrors | null { + if (this.itemNames && control.value != this.itemNames[0]) { + return { matchResource: true }; + } + return null; + } + callSubmitAction() { if (this.submitActionObservable) { this.submitActionObservable().subscribe({ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/critical-confirmation-modal-impact.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/critical-confirmation-modal-impact.enum.ts new file mode 100644 index 00000000000..045d6f52ae6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/critical-confirmation-modal-impact.enum.ts @@ -0,0 +1,4 @@ +export enum DeletionImpact { + normal = 'normal', + high = 'high' // NOTE: User should be able to select only single resource while deleting +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 14b1df5b334..05f7c460712 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -323,9 +323,7 @@ export class TaskMessageService { ), // RGW operations 'rgw/bucket/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) => { - return $localize`${ - metadata.bucket_names.length > 1 ? 'selected buckets' : metadata.bucket_names[0] - }`; + return $localize`${metadata.bucket_names[0]}`; }), 'rgw/accounts': this.newTaskMessage(this.commonOperations.delete, (metadata) => { return $localize`${`account '${metadata.account_names[0]}'`}`;