]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Add confirmation textbox for resource name on delete action
authorNaman Munet <naman.munet@ibm.com>
Wed, 22 Jan 2025 10:59:20 +0000 (16:29 +0530)
committerNaman Munet <naman.munet@ibm.com>
Mon, 24 Feb 2025 12:30:56 +0000 (18:00 +0530)
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 <naman.munet@ibm.com>
(cherry picked from commit ed222df10900198707d7708518e02af5c06c912b)

24 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/critical-confirmation-modal-impact.enum.ts [new file with mode: 0644]

index 61f812177561ca364c801923c5796c6f9b3ab842..5916a20e57e3e779aa756f803338e459205ef848 100644 (file)
@@ -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: () =>
index 8fc36a4cb479d64fb3680f4fb5815fb1c9f90038..61b7866eef6707b6fcf40fcef6471e1fc4fc331e 100644 (file)
@@ -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(),
index da8a185ea1cb7739358e2dbbe1265a678ad7b880..5f12821aaa59c5e9ac521796ce3da9df16fa3b94 100644 (file)
@@ -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)
index c4c4728bab5da21a5423c00c61180172ef8de226..91518ed55fa7459a8672433c7ddfce4059237d8a 100644 (file)
@@ -54,7 +54,7 @@
                   [columns]="snapshot.columns"
                   identifier="name"
                   forceIdentifier="true"
-                  selectionType="multiClick"
+                  selectionType="single"
                   (updateSelection)="snapshot.updateSelection($event)">
           <cd-table-actions class="table-actions"
                             [permission]="permission"
index 387a21b6f5769b158a75a76728791c6b29fd9e4d..8d24bd567db3abbbecb88c6b019aee8d06d4b6d5 100644 (file)
@@ -17,6 +17,7 @@ import { ConfirmationModalComponent } from '~/app/shared/components/confirmation
 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+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 { CdValidators } from '~/app/shared/forms/cd-validators';
@@ -232,9 +233,7 @@ export class CephfsDirectoriesComponent implements OnInit, OnChanges {
           name: this.actionLabels.DELETE,
           icon: Icons.destroy,
           permission: 'delete',
-          click: () => 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()
index 3ab50d62f4774db61919cad299c873b898ead731..5caa4dd9c51a7401444152fc82006130c0f539de 100644 (file)
@@ -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',
index a8b62556f28bebfd31dcf65310bf4b57ec558719..10d155fab023cce2158af27548738e1738cbb907 100644 (file)
@@ -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',
index 2d646f968dc543a4ffe78616cfcfd77d8c4c0bc7..64b71f9024640cb86da37c0452a6df19d7037d29 100644 (file)
@@ -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',
index d0bf2aaa18a832c1ea6e937e9cd216acacb7d1c0..cc6e6e0df5bea27b3cf0e5319c31b5521ede2bf2 100644 (file)
@@ -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: () =>
index 0caeac9f2eb8f797bffc7d58f3dd89fa4bc777d1..082aef10780caed397b03cfd3f59f5deae65a39e 100644 (file)
@@ -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',
index b32e78c56a8af0178b48792f9cd13cb2555bf065..0c64cdbfc301011548aad80a8d798a61aa7a7f00 100644 (file)
@@ -8,7 +8,7 @@
       <cd-table [data]="osds"
                 (fetchData)="getOsdList($event)"
                 [columns]="columns"
-                selectionType="multiClick"
+                selectionType="single"
                 [hasDetails]="true"
                 (setExpandedRow)="setExpandedRow($event)"
                 (updateSelection)="updateSelection($event)"
index 434c30588ad39b93d26715592a619ba37491d0bd..592de5fb73ec9ed2b6335df4b904e356e260e438 100644 (file)
@@ -40,6 +40,7 @@ import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-mo
 import { OsdScrubModalComponent } from '../osd-scrub-modal/osd-scrub-modal.component';
 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
 import { Osd } from '~/app/shared/models/osd.model';
+import { DeletionImpact } from '~/app/shared/enum/critical-confirmation-modal-impact.enum';
 
 const BASE_URL = 'osd';
 
@@ -579,7 +580,10 @@ export class OsdListComponent extends ListWithDetails implements OnInit {
     childFormGroupTemplate?: TemplateRef<any>
   ): 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,
index 82a975c9df47ed58f6de09ec861a47b4e5a8ee0e..a3f6ef90907819906e7b1db3b3a76e5cd76f9929 100644 (file)
@@ -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',
index d5d0c2639300c322820a59b3c539c97942828aa2..354f0186334d6d9f792be82e995e7cb145bc72e6 100644 (file)
@@ -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: () =>
index ba2d9cbe521b1399ffb459695a7fecc2ac2f4519..2696a9d5e68139c08532e19c3e98c0b77ed2eece 100644 (file)
@@ -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: () =>
index b5e75841afe639a90b6bc657e8d80442d473bfa1..2d6e7252a4dd9d34ca92c52280d2a35004ceb45c 100644 (file)
@@ -3,7 +3,7 @@
           [data]="buckets"
           [columns]="columns"
           columnMode="flex"
-          selectionType="multiClick"
+          selectionType="single"
           [hasDetails]="true"
           (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)"
index fdb6217fd4469cbd587519bb0e695f7eceb76a74..e00895433d8df3b2d8ca607b4cdf884053a6bfde 100644 (file)
@@ -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<any>) => {
           // Delete all selected data table rows.
index 8f50e4abcb241077c3a62ac71bcbd41b658a742a..4413afe28a86ed0543ecc177a8d63abd9a3c8f71 100644 (file)
@@ -5,7 +5,7 @@
           [data]="users"
           [columns]="columns"
           columnMode="flex"
-          selectionType="multiClick"
+          selectionType="single"
           [hasDetails]="true"
           (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)"
index 3c0f9264d80168de705554c9189048bafdbccb04..4f7caedcea9ac1783a2e24a4d3d150d769098ae4 100644 (file)
@@ -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<any> => {
index 3a16fdce610d003e72e33ff0b14aa7b2dde484bc..1941cf3566dd55beb462b7007de277e7c8fff0d0 100755 (executable)
@@ -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)
index cc2eded0e3b8f79d9b0d9d36924c2306eea6c15d..5f7988a60d48ba7ee73594922e43fa09fce61902 100644 (file)
           </ng-template>
           <ng-container *ngTemplateOutlet="childFormGroupTemplate; context:{form:deletionForm}"></ng-container>
           <div class="form-group">
-            <div class="custom-control custom-checkbox">
-              <input type="checkbox"
-                     class="custom-control-input"
-                     name="confirmation"
-                     id="confirmation"
-                     formControlName="confirmation"
-                     autofocus>
-              <label class="custom-control-label"
-                     for="confirmation"
-                     i18n>Yes, I am sure.</label>
-            </div>
+            <ng-container *ngIf="impact == impactEnum.medium; else highImpactDeletion">
+              <div class="custom-control custom-checkbox">
+                <input type="checkbox"
+                       class="custom-control-input"
+                       name="confirmation"
+                       id="confirmation"
+                       formControlName="confirmation"
+                       autofocus>
+                <label class="custom-control-label"
+                       for="confirmation"
+                       i18n>Yes, I am sure.</label>
+              </div>
+            </ng-container>
+            <ng-template #highImpactDeletion>
+              <label i18n
+                     for="name">Resource Name</label>
+              <div class="cd-col-form-input">
+                <input class="form-control"
+                       type="text"
+                       placeholder="Enter resource name to delete"
+                       id="resource_name"
+                       name="resource_name"
+                       formControlName="confirmInput">
+                <span *ngIf="deletionForm.showError('confirmInput', formDir, 'required')"
+                      class="invalid-feedback">
+                  <ng-container i18n>This field is required.</ng-container>
+                </span>
+                <span *ngIf="deletionForm.showError('confirmInput', formDir, 'matchResource')"
+                      class="invalid-feedback">
+                  <ng-container i18n>Enter the correct resource name.</ng-container>
+                </span>
+              </div>
+            </ng-template>
           </div>
         </div>
       </div>
@@ -53,4 +75,4 @@
 
 <ng-template #deletionHeading>
   {{ actionDescription | titlecase }} {{ itemDescription }}
-</ng-template>
+</ng-template>
\ No newline at end of file
index e501d9f329ad61b61a0ff85050797772900c2b0f..066a7f4592ef7baf9ef7bffb9c3aa4b640ba4444 100644 (file)
@@ -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', <NgForm>{ 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', () => {
index 406f992a9df22a7aeed2ea3264044b8023e013d1..9280d818252304cd17e686bab12701b7d464ba0e 100644 (file)
@@ -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<any>;
-
-  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 (file)
index 0000000..045d6f5
--- /dev/null
@@ -0,0 +1,4 @@
+export enum DeletionImpact {
+  normal = 'normal',
+  high = 'high' // NOTE: User should be able to select only single resource while deleting
+}