From 91def255e0b5167fa612d8be78c4d65b006614b1 Mon Sep 17 00:00:00 2001 From: Naman Munet Date: Wed, 22 Jan 2025 16:29:20 +0530 Subject: [PATCH] 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 (cherry picked from commit ed222df10900198707d7708518e02af5c06c912b) --- .../pool-list/pool-list.component.ts | 2 + .../ceph/block/rbd-list/rbd-list.component.ts | 5 +- .../rbd-snapshot-list.component.ts | 2 + .../cephfs-directories.component.html | 2 +- .../cephfs-directories.component.ts | 6 +-- .../cephfs-list/cephfs-list.component.ts | 2 + .../cephfs-subvolume-group.component.ts | 2 + .../cephfs-subvolume-list.component.ts | 2 + ...phfs-subvolume-snapshots-list.component.ts | 4 +- .../app/ceph/cluster/hosts/hosts.component.ts | 2 + .../osd/osd-list/osd-list.component.html | 2 +- .../osd/osd-list/osd-list.component.ts | 6 ++- .../cluster/services/services.component.ts | 2 + .../ceph/nfs/nfs-list/nfs-list.component.ts | 2 + .../pool/pool-list/pool-list.component.ts | 2 + .../rgw-bucket-list.component.html | 2 +- .../rgw-bucket-list.component.ts | 11 ++-- .../rgw-user-list.component.html | 2 +- .../rgw-user-list/rgw-user-list.component.ts | 5 +- .../auth/user-list/user-list.component.ts | 2 + ...critical-confirmation-modal.component.html | 46 ++++++++++++----- ...tical-confirmation-modal.component.spec.ts | 50 +++++++++++++++++-- .../critical-confirmation-modal.component.ts | 39 ++++++++++++--- ...critical-confirmation-modal-impact.enum.ts | 4 ++ 24 files changed, 163 insertions(+), 41 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/enum/critical-confirmation-modal-impact.enum.ts 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 61f812177561c..5916a20e57e3e 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 @@ -17,6 +17,7 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { ModalService } from '~/app/shared/services/modal.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { PoolEditPeerModalComponent } from '../pool-edit-peer-modal/pool-edit-peer-modal.component'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = '/block/mirroring'; @Component({ @@ -150,6 +151,7 @@ export class PoolListComponent implements OnInit, OnDestroy { const peerUUID = this.getPeerUUID(); this.modalRef = 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 8fc36a4cb479d..61b7866eef670 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,7 +32,7 @@ import { RbdFormEditRequestModel } from '../rbd-form/rbd-form-edit-request.model import { RbdParentModel } from '../rbd-form/rbd-parent.model'; import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component'; import { RBDImageFormat, RbdModel } from './rbd-model'; - +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = 'block/rbd'; @Component({ @@ -428,8 +428,9 @@ export class RbdListComponent extends ListWithDetails implements OnInit { const imageSpec = new ImageSpec(poolName, namespace, imageName); this.modalRef = this.modalService.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 da8a185ea1cb7..5f12821aaa59c 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 @@ -38,6 +38,7 @@ import { TaskManagerService } from '~/app/shared/services/task-manager.service'; import { RbdSnapshotFormModalComponent } from '../rbd-snapshot-form/rbd-snapshot-form-modal.component'; import { RbdSnapshotActionsModel } from './rbd-snapshot-actions.model'; import { RbdSnapshotModel } from './rbd-snapshot.model'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-rbd-snapshot-list', @@ -326,6 +327,7 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { deleteSnapshotModal() { const snapshotName = this.selection.selected[0].name; this.modalRef = this.modalService.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 c4c4728bab5da..91518ed55fa74 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 @@ -54,7 +54,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() } ] }; @@ -696,6 +695,7 @@ export class CephfsDirectoriesComponent implements OnInit, OnChanges { deleteSnapshotModal() { this.modalRef = 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 3ab50d62f4774..5caa4dd9c51a7 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 { CephfsMountDetailsComponent } from '../cephfs-mount-details/cephfs-moun 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 { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = 'cephfs'; @@ -172,6 +173,7 @@ export class CephfsListComponent extends ListWithDetails implements OnInit { removeVolumeModal() { const volName = this.selection.first().mdsmap['fs_name']; this.modalService.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 a8b62556f28be..10d155fab023c 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 { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-cephfs-subvolume-group', @@ -178,6 +179,7 @@ export class CephfsSubvolumeGroupComponent implements OnInit, OnChanges { removeSubVolumeModal() { const name = this.selection.first().name; this.modalService.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 2d646f968dc54..64b71f9024640 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 @@ -34,6 +34,7 @@ import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group import { CephfsMountDetailsComponent } from '../cephfs-mount-details/cephfs-mount-details.component'; import { HealthService } from '~/app/shared/api/health.service'; import _ from 'lodash'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const DEFAULT_SUBVOLUME_GROUP = '_nogroup'; @@ -251,6 +252,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh this.errorMessage = ''; this.selectedName = this.selection.first().name; this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, actionDescription: 'Remove', itemNames: [this.selectedName], itemDescription: 'Subvolume', 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 d0bf2aaa18a83..cc6e6e0df5bea 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 moment from 'moment'; import { Validators } from '@angular/forms'; import { CdValidators } from '~/app/shared/forms/cd-validators'; import { DEFAULT_SUBVOLUME_GROUP } from '~/app/shared/constants/cephfs'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-cephfs-subvolume-snapshots-list', @@ -232,7 +233,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 0caeac9f2eb8f..082aef10780ca 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 @@ -35,6 +35,7 @@ import { NotificationService } from '~/app/shared/services/notification.service' 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 { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = 'hosts'; @@ -447,6 +448,7 @@ export class HostsComponent extends ListWithDetails implements OnDestroy, OnInit deleteAction() { const hostname = this.selection.first().hostname; this.modalRef = this.modalService.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 b32e78c56a8af..0c64cdbfc3010 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(); const modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + impact: DeletionImpact.high, + itemNames: osdIds, actionDescription: actionDescription, itemDescription: itemDescription, bodyTemplate: this.criticalConfirmationTpl, @@ -590,7 +594,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 82a975c9df47e..a3f6ef9090781 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 @@ -27,6 +27,7 @@ import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { URLBuilderService } from '~/app/shared/services/url-builder.service'; import { PlacementPipe } from './placement.pipe'; import { ServiceFormComponent } from './service-form/service-form.component'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; const BASE_URL = 'services'; @@ -238,6 +239,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI deleteAction() { const service = this.selection.first(); this.modalService.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 d5d0c2639300c..354f0186334d6 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 @@ -22,6 +22,7 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { ModalService } from '~/app/shared/services/modal.service'; import { TaskListService } from '~/app/shared/services/task-list.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-nfs-list', @@ -184,6 +185,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 ba2d9cbe521b1..2696a9d5e6813 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 { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { URLBuilderService } from '~/app/shared/services/url-builder.service'; import { Pool } from '../pool'; import { PoolStat, PoolStats } from '../pool-stat'; +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 b5e75841afe63..2d6e7252a4dd9 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 fdb6217fd4469..e00895433d8df 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 @@ -21,6 +21,7 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { ModalService } from '~/app/shared/services/modal.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'; @@ -116,9 +117,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(); @@ -144,9 +143,11 @@ 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`, - itemNames: this.selection.selected.map((bucket: any) => bucket['bid']), + itemDescription: $localize`bucket`, + impact: DeletionImpact.high, + itemNames: itemNames, submitActionObservable: () => { return new Observable((observer: Subscriber) => { // Delete all selected data table rows. 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 8f50e4abcb241..4413afe28a86e 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 3c0f9264d8016..4f7caedcea9ac 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 3a16fdce610d0..1941cf3566dd5 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 cc2eded0e3b8f..5f7988a60d48b 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 @@ -27,17 +27,39 @@
-
- - -
+ +
+ + +
+
+ + +
+ + + This field is required. + + + Enter the correct resource name. + +
+
@@ -53,4 +75,4 @@ {{ actionDescription | titlecase }} {{ itemDescription }} - + \ No newline at end of file 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 e501d9f329ad6..066a7f4592ef7 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 { configureTestBed, modalServiceShow } from '~/testing/unit-test-helper'; import { AlertPanelComponent } from '../alert-panel/alert-panel.component'; import { LoadingPanelComponent } from '../loading-panel/loading-panel.component'; import { CriticalConfirmationModalComponent } from './critical-confirmation-modal.component'; +import { DeletionImpact } from '../../enum/critical-confirmation-modal-impact.enum'; @NgModule({}) export class MockModule {} @@ -141,8 +142,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,17 +168,56 @@ 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(false, undefined, false); 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 406f992a9df22..9280d81825230 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,13 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { UntypedFormControl, Validators } from '@angular/forms'; +import { AbstractControl, UntypedFormControl, ValidationErrors, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { Observable } from 'rxjs'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { SubmitButtonComponent } from '../submit-button/submit-button.component'; +import { CdValidators } from '../../forms/cd-validators'; +import { DeletionImpact } from '../../enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-deletion-modal', @@ -25,15 +27,33 @@ export class CriticalConfirmationModalComponent implements OnInit { itemDescription: 'entry'; itemNames: string[]; actionDescription = 'delete'; - + impactEnum = DeletionImpact; childFormGroup: CdFormGroup; childFormGroupTemplate: TemplateRef; - - constructor(public activeModal: NgbActiveModal) {} + impact: DeletionImpact; + constructor(public activeModal: NgbActiveModal) { + 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; @@ -44,6 +64,13 @@ export class CriticalConfirmationModalComponent implements OnInit { } } + matchResourceName(control: AbstractControl): ValidationErrors | null { + if (this.itemNames && control.value !== String(this.itemNames?.[0])) { + return { matchResource: true }; + } + return null; + } + callSubmitAction() { if (this.submitActionObservable) { this.submitActionObservable().subscribe({ @@ -73,4 +100,4 @@ export class CriticalConfirmationModalComponent implements OnInit { stopLoadingSpinner() { this.deletionForm.setErrors({ cdSubmitButton: true }); } -} +} \ No newline at end of file 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 0000000000000..045d6f52ae6c7 --- /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 +} -- 2.39.5