From 0ec1d36a6bdd374386ea0013badb0964327c8d70 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 | 4 +- .../ceph/block/rbd-list/rbd-list.component.ts | 6 ++- .../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 | 7 +++ .../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 | 44 ++++++++++++----- ...tical-confirmation-modal.component.spec.ts | 49 +++++++++++++++++-- .../critical-confirmation-modal.component.ts | 36 ++++++++++++-- ...critical-confirmation-modal-impact.enum.ts | 4 ++ 24 files changed, 169 insertions(+), 37 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..a51f983340ab3 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({ @@ -149,7 +150,8 @@ export class PoolListComponent implements OnInit, OnDestroy { const poolName = this.selection.first().name; const peerUUID = this.getPeerUUID(); - this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + 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..bfa053149d870 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 @@ -33,6 +33,9 @@ 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 { 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({ @@ -428,8 +431,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() } ] }; @@ -692,6 +691,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 748eeee0ee4c4..7b6ee3aac9ce1 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/fs'; @@ -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..f1815b0dff950 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 @@ -36,6 +36,7 @@ import { HealthService } from '~/app/shared/api/health.service'; import _ from 'lodash'; const DEFAULT_SUBVOLUME_GROUP = '_nogroup'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; @Component({ selector: 'cd-cephfs-subvolume-list', @@ -252,6 +253,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh this.selectedName = this.selection.first().name; this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { actionDescription: 'Remove', + impact: DeletionImpact.high, 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 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 fef729de58a0e..2945a79cd1cf1 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 { 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'; @@ -468,6 +469,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 8be95c6febe79..fe53b38786be0 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,12 @@ import { TaskListService } from '~/app/shared/services/task-list.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { getFsalFromRoute, getPathfromFsal } from '../utils'; import { SUPPORTED_FSAL } from '../models/nfs.fsal'; +import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum'; + +export enum RgwExportType { + BUCKET = 'bucket', + USER = 'user' +} @Component({ selector: 'cd-nfs-list', @@ -191,6 +197,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..cd0bc956c7cd8 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 @@
-
- - -
+ + 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 e501d9f329ad6..4210fc777eff8 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,57 @@ 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..ca7b42358dc5b 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 @@ -6,6 +6,9 @@ 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', @@ -25,15 +28,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 +65,13 @@ export class CriticalConfirmationModalComponent implements OnInit { } } + 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 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