From: Patrick Nawracay Date: Mon, 15 Oct 2018 13:23:29 +0000 (+0200) Subject: mgr/dashboard: Rename `DeletionModalComponent` to `CriticalConfirmationModalComponent` X-Git-Tag: v14.1.0~1083^2~3 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=e8218ac14590cdcc36d1e93120b9c77a58108e47;p=ceph.git mgr/dashboard: Rename `DeletionModalComponent` to `CriticalConfirmationModalComponent` Signed-off-by: Patrick Nawracay --- 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 d0498dc3cd7..346bdb40ef2 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 @@ -5,7 +5,7 @@ import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { RbdService } from '../../../shared/api/rbd.service'; import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component'; -import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { TableComponent } from '../../../shared/datatable/table/table.component'; import { CellTemplate } from '../../../shared/enum/cell-template.enum'; import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; @@ -256,7 +256,7 @@ export class RbdListComponent implements OnInit { const poolName = this.selection.first().pool_name; const imageName = this.selection.first().name; - this.modalRef = this.modalService.show(DeletionModalComponent, { + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { initialState: { itemDescription: 'RBD', submitActionObservable: () => 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 014ecc447a5..f6d3cbd7067 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 @@ -6,7 +6,7 @@ import { of } from 'rxjs'; import { RbdService } from '../../../shared/api/rbd.service'; import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component'; -import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { CellTemplate } from '../../../shared/enum/cell-template.enum'; import { CdTableAction } from '../../../shared/models/cd-table-action'; import { CdTableColumn } from '../../../shared/models/cd-table-column'; @@ -269,7 +269,7 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { deleteSnapshotModal() { const snapshotName = this.selection.selected[0].name; - this.modalRef = this.modalService.show(DeletionModalComponent, { + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { initialState: { itemDescription: 'RBD snapshot', submitAction: () => this._asyncTask('deleteSnapshot', 'rbd/snap/delete', snapshotName) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts index 7c569757b70..ac6059d5bbc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts @@ -5,7 +5,7 @@ import * as moment from 'moment'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { RbdService } from '../../../shared/api/rbd.service'; -import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { TableComponent } from '../../../shared/datatable/table/table.component'; import { CellTemplate } from '../../../shared/enum/cell-template.enum'; import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; @@ -177,7 +177,7 @@ export class RbdTrashListComponent implements OnInit { const imageId = this.selection.first().id; const expiresAt = this.selection.first().deferment_end_time; - this.modalRef = this.modalService.show(DeletionModalComponent, { + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { initialState: { itemDescription: 'RBD', bodyTemplate: this.deleteTpl, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts index ee549602aec..f4992636280 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { OsdService } from '../../../../shared/api/osd.service'; +import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component'; import { TableComponent } from '../../../../shared/datatable/table/table.component'; import { CellTemplate } from '../../../../shared/enum/cell-template.enum'; import { CdTableAction } from '../../../../shared/models/cd-table-action'; 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 9faa989df7c..6e1c2ea976e 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 @@ -3,7 +3,6 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { PoolService } from '../../../shared/api/pool.service'; -import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component'; import { TableComponent } from '../../../shared/datatable/table/table.component'; import { CellTemplate } from '../../../shared/enum/cell-template.enum'; import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; @@ -17,6 +16,7 @@ import { AuthStorageService } from '../../../shared/services/auth-storage.servic import { TaskListService } from '../../../shared/services/task-list.service'; import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; import { Pool } from '../pool'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; @Component({ selector: 'cd-pool-list', @@ -133,7 +133,7 @@ export class PoolListComponent implements OnInit { deletePoolModal() { const name = this.selection.first().pool_name; - this.modalRef = this.modalService.show(DeletionModalComponent, { + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { initialState: { itemDescription: 'Pool', submitActionObservable: () => 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 1c9f4b6e3cf..170835f05db 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 @@ -4,7 +4,7 @@ import { BsModalService } from 'ngx-bootstrap/modal'; import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs'; import { RgwBucketService } from '../../../shared/api/rgw-bucket.service'; -import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { TableComponent } from '../../../shared/datatable/table/table.component'; import { CdTableAction } from '../../../shared/models/cd-table-action'; import { CdTableColumn } from '../../../shared/models/cd-table-column'; @@ -85,7 +85,7 @@ export class RgwBucketListComponent { } deleteAction() { - this.bsModalService.show(DeletionModalComponent, { + this.bsModalService.show(CriticalConfirmationModalComponent, { initialState: { itemDescription: this.selection.hasSingleSelection ? 'bucket' : 'buckets', submitActionObservable: () => { 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 32f820ef76b..29397ebdb96 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 @@ -4,7 +4,7 @@ import { BsModalService } from 'ngx-bootstrap/modal'; import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs'; import { RgwUserService } from '../../../shared/api/rgw-user.service'; -import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { TableComponent } from '../../../shared/datatable/table/table.component'; import { CellTemplate } from '../../../shared/enum/cell-template.enum'; import { CdTableAction } from '../../../shared/models/cd-table-action'; @@ -101,7 +101,7 @@ export class RgwUserListComponent { } deleteAction() { - this.bsModalService.show(DeletionModalComponent, { + this.bsModalService.show(CriticalConfirmationModalComponent, { initialState: { itemDescription: this.selection.hasSingleSelection ? 'user' : 'users', submitActionObservable: (): Observable => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts index 427715b2fdd..2f1836408ef 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts @@ -5,7 +5,7 @@ import { forkJoin } from 'rxjs'; import { RoleService } from '../../../shared/api/role.service'; import { ScopeService } from '../../../shared/api/scope.service'; -import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { EmptyPipe } from '../../../shared/empty.pipe'; import { CellTemplate } from '../../../shared/enum/cell-template.enum'; import { NotificationType } from '../../../shared/enum/notification-type.enum'; @@ -115,7 +115,7 @@ export class RoleListComponent implements OnInit { deleteRoleModal() { const name = this.selection.first().name; - this.modalRef = this.modalService.show(DeletionModalComponent, { + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { initialState: { itemDescription: 'Role', submitAction: () => this.deleteRole(name) 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 47b2882c24a..1152c7e795d 100644 --- 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 @@ -3,7 +3,7 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { UserService } from '../../../shared/api/user.service'; -import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { EmptyPipe } from '../../../shared/empty.pipe'; import { NotificationType } from '../../../shared/enum/notification-type.enum'; import { CdTableAction } from '../../../shared/models/cd-table-action'; @@ -122,7 +122,7 @@ export class UserListComponent implements OnInit { ); return; } - this.modalRef = this.modalService.show(DeletionModalComponent, { + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { initialState: { itemDescription: 'User', submitAction: () => this.deleteUser(username) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts index 9c33a3d8936..9ad9d4895ec 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -12,7 +12,7 @@ import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { DirectivesModule } from '../directives/directives.module'; import { PipesModule } from '../pipes/pipes.module'; import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component'; -import { DeletionModalComponent } from './deletion-modal/deletion-modal.component'; +import { CriticalConfirmationModalComponent } from './critical-confirmation-modal/critical-confirmation-modal.component'; import { ErrorPanelComponent } from './error-panel/error-panel.component'; import { GrafanaComponent } from './grafana/grafana.component'; import { HelperComponent } from './helper/helper.component'; @@ -52,7 +52,7 @@ import { WarningPanelComponent } from './warning-panel/warning-panel.component'; LoadingPanelComponent, InfoPanelComponent, ModalComponent, - DeletionModalComponent, + CriticalConfirmationModalComponent, ConfirmationModalComponent, WarningPanelComponent, GrafanaComponent @@ -72,6 +72,10 @@ import { WarningPanelComponent } from './warning-panel/warning-panel.component'; WarningPanelComponent, GrafanaComponent ], - entryComponents: [ModalComponent, DeletionModalComponent, ConfirmationModalComponent] + entryComponents: [ + ModalComponent, + CriticalConfirmationModalComponent, + ConfirmationModalComponent + ] }) export class ComponentsModule {} 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 new file mode 100644 index 00000000000..d12d9006bba --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html @@ -0,0 +1,54 @@ + + + + + + +
+ + +
+
+
+ + + + {{ actionDescription | titlecase }} {{ itemDescription }} + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.scss new file mode 100644 index 00000000000..aff598d9b55 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.scss @@ -0,0 +1,6 @@ +.modal-body .question { + font-weight: bold; +} +.modal-body .question .checkbox { + padding-top: 7px; +} 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 new file mode 100644 index 00000000000..0189fce78e9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts @@ -0,0 +1,254 @@ +import { Component, NgModule, NO_ERRORS_SCHEMA, TemplateRef, ViewChild } from '@angular/core'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { NgForm, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; + +import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap/modal'; +import { Observable, Subscriber, timer as observableTimer } from 'rxjs'; + +import { configureTestBed } from '../../../../testing/unit-test-helper'; +import { DirectivesModule } from '../../directives/directives.module'; +import { CriticalConfirmationModalComponent } from './critical-confirmation-modal.component'; + +@NgModule({ + entryComponents: [CriticalConfirmationModalComponent] +}) +export class MockModule {} + +@Component({ + template: ` + + + + ` +}) +class MockComponent { + @ViewChild('ctrlDescription') + ctrlDescription: TemplateRef; + @ViewChild('modalDescription') + modalDescription: TemplateRef; + someData = [1, 2, 3, 4, 5]; + finished: number[]; + ctrlRef: BsModalRef; + modalRef: BsModalRef; + + // Normally private - public was needed for the tests + constructor(public modalService: BsModalService) {} + + openCtrlDriven() { + this.ctrlRef = this.modalService.show(CriticalConfirmationModalComponent, { + initialState: { + submitAction: this.fakeDeleteController.bind(this), + bodyTemplate: this.ctrlDescription + } + }); + } + + openModalDriven() { + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + initialState: { + submitActionObservable: this.fakeDelete(), + bodyTemplate: this.modalDescription + } + }); + } + + finish() { + this.finished = [6, 7, 8, 9]; + } + + fakeDelete() { + return (): Observable => { + return new Observable((observer: Subscriber) => { + observableTimer(100).subscribe(() => { + observer.next(this.finish()); + observer.complete(); + }); + }); + }; + } + + fakeDeleteController() { + observableTimer(100).subscribe(() => { + this.finish(); + this.ctrlRef.hide(); + }); + } +} + +describe('DeletionModalComponent', () => { + let mockComponent: MockComponent; + let component: CriticalConfirmationModalComponent; + let mockFixture: ComponentFixture; + let fixture: ComponentFixture; + + configureTestBed({ + declarations: [MockComponent, CriticalConfirmationModalComponent], + schemas: [NO_ERRORS_SCHEMA], + imports: [ModalModule.forRoot(), ReactiveFormsModule, MockModule, DirectivesModule], + providers: [BsModalRef] + }); + + beforeEach(() => { + mockFixture = TestBed.createComponent(MockComponent); + mockComponent = mockFixture.componentInstance; + // Mocking the modals as a lot would be left over + spyOn(mockComponent.modalService, 'show').and.callFake((modalComp, config) => { + const ref = new BsModalRef(); + fixture = TestBed.createComponent(CriticalConfirmationModalComponent); + component = fixture.componentInstance; + if (config.initialState) { + component = Object.assign(component, config.initialState); + } + fixture.detectChanges(); + ref.content = component; + return ref; + }); + mockComponent.openCtrlDriven(); + mockFixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should focus the checkbox form field', () => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + const focused = fixture.debugElement.query(By.css(':focus')); + expect(focused.attributes.id).toBe('confirmation'); + expect(focused.attributes.type).toBe('checkbox'); + const element = document.getElementById('confirmation'); + expect(element === document.activeElement).toBeTruthy(); + }); + }); + + it('should throw an error if no action is defined', () => { + component = Object.assign(component, { + submitAction: null, + submitActionObservable: null + }); + expect(() => component.ngOnInit()).toThrowError('No submit action defined'); + }); + + it('should test if the ctrl driven mock is set correctly through mock component', () => { + expect(component.bodyTemplate).toBeTruthy(); + expect(component.submitAction).toBeTruthy(); + expect(component.submitActionObservable).not.toBeTruthy(); + }); + + it('should test if the modal driven mock is set correctly through mock component', () => { + mockComponent.openModalDriven(); + expect(component.bodyTemplate).toBeTruthy(); + expect(component.submitActionObservable).toBeTruthy(); + expect(component.submitAction).not.toBeTruthy(); + }); + + describe('component functions', () => { + const changeValue = (value) => { + const ctrl = component.deletionForm.get('confirmation'); + ctrl.setValue(value); + ctrl.markAsDirty(); + ctrl.updateValueAndValidity(); + fixture.detectChanges(); + }; + + it('should test hideModal', () => { + expect(component.modalRef).toBeTruthy(); + expect(component.hideModal).toBeTruthy(); + spyOn(component.modalRef, 'hide').and.callThrough(); + expect(component.modalRef.hide).not.toHaveBeenCalled(); + component.hideModal(); + expect(component.modalRef.hide).toHaveBeenCalled(); + }); + + describe('validate confirmation', () => { + const testValidation = (submitted: boolean, error: string, expected: boolean) => { + expect( + component.deletionForm.showError('confirmation', { submitted: submitted }, error) + ).toBe(expected); + }; + + beforeEach(() => { + component.deletionForm.reset(); + }); + + it('should test empty values', () => { + component.deletionForm.reset(); + testValidation(false, undefined, false); + testValidation(true, 'required', true); + component.deletionForm.reset(); + changeValue(true); + changeValue(false); + testValidation(true, 'required', true); + }); + }); + + describe('deletion call', () => { + beforeEach(() => { + spyOn(component, 'stopLoadingSpinner').and.callThrough(); + spyOn(component, 'hideModal').and.callThrough(); + }); + + describe('Controller driven', () => { + beforeEach(() => { + spyOn(component, 'submitAction').and.callThrough(); + spyOn(mockComponent.ctrlRef, 'hide').and.callThrough(); + }); + + it('should test fake deletion that closes modal', fakeAsync(() => { + // Before deletionCall + expect(component.submitAction).not.toHaveBeenCalled(); + // During deletionCall + component.callSubmitAction(); + expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); + expect(component.hideModal).not.toHaveBeenCalled(); + expect(mockComponent.ctrlRef.hide).not.toHaveBeenCalled(); + expect(component.submitAction).toHaveBeenCalled(); + expect(mockComponent.finished).toBe(undefined); + // After deletionCall + tick(2000); + expect(component.hideModal).not.toHaveBeenCalled(); + expect(mockComponent.ctrlRef.hide).toHaveBeenCalled(); + expect(mockComponent.finished).toEqual([6, 7, 8, 9]); + })); + }); + + describe('Modal driven', () => { + beforeEach(() => { + mockComponent.openModalDriven(); + spyOn(component, 'stopLoadingSpinner').and.callThrough(); + spyOn(component, 'hideModal').and.callThrough(); + spyOn(mockComponent, 'fakeDelete').and.callThrough(); + }); + + it('should delete and close modal', fakeAsync(() => { + // During deletionCall + component.callSubmitAction(); + expect(mockComponent.finished).toBe(undefined); + expect(component.hideModal).not.toHaveBeenCalled(); + // After deletionCall + tick(2000); + expect(mockComponent.finished).toEqual([6, 7, 8, 9]); + expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); + expect(component.hideModal).toHaveBeenCalled(); + })); + }); + }); + }); +}); 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 new file mode 100644 index 00000000000..27d39f3b77a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { Observable } from 'rxjs'; + +import { CdFormGroup } from '../../forms/cd-form-group'; +import { SubmitButtonComponent } from '../submit-button/submit-button.component'; + +@Component({ + selector: 'cd-deletion-modal', + templateUrl: './critical-confirmation-modal.component.html', + styleUrls: ['./critical-confirmation-modal.component.scss'] +}) +export class CriticalConfirmationModalComponent implements OnInit { + @ViewChild(SubmitButtonComponent) + submitButton: SubmitButtonComponent; + bodyTemplate: TemplateRef; + bodyContext: any; + submitActionObservable: () => Observable; + submitAction: Function; + deletionForm: CdFormGroup; + itemDescription: 'entry'; + actionDescription = 'delete'; + + constructor(public modalRef: BsModalRef) {} + + ngOnInit() { + this.deletionForm = new CdFormGroup({ + confirmation: new FormControl(false, [Validators.requiredTrue]) + }); + + if (!(this.submitAction || this.submitActionObservable)) { + throw new Error('No submit action defined'); + } + } + + callSubmitAction() { + if (this.submitActionObservable) { + this.submitActionObservable().subscribe( + null, + this.stopLoadingSpinner.bind(this), + this.hideModal.bind(this) + ); + } else { + this.submitAction(); + } + } + + hideModal() { + this.modalRef.hide(); + } + + stopLoadingSpinner() { + this.deletionForm.setErrors({ cdSubmitButton: true }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.html deleted file mode 100644 index d12d9006bba..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - -
- - -
-
-
- - - - {{ actionDescription | titlecase }} {{ itemDescription }} - - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.scss deleted file mode 100644 index aff598d9b55..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -.modal-body .question { - font-weight: bold; -} -.modal-body .question .checkbox { - padding-top: 7px; -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.spec.ts deleted file mode 100644 index 3115a6a95e2..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.spec.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { Component, NgModule, NO_ERRORS_SCHEMA, TemplateRef, ViewChild } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { NgForm, ReactiveFormsModule } from '@angular/forms'; -import { By } from '@angular/platform-browser'; - -import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap/modal'; -import { Observable, Subscriber, timer as observableTimer } from 'rxjs'; - -import { configureTestBed } from '../../../../testing/unit-test-helper'; -import { DirectivesModule } from '../../directives/directives.module'; -import { DeletionModalComponent } from './deletion-modal.component'; - -@NgModule({ - entryComponents: [DeletionModalComponent] -}) -export class MockModule {} - -@Component({ - template: ` - - - - ` -}) -class MockComponent { - @ViewChild('ctrlDescription') - ctrlDescription: TemplateRef; - @ViewChild('modalDescription') - modalDescription: TemplateRef; - someData = [1, 2, 3, 4, 5]; - finished: number[]; - ctrlRef: BsModalRef; - modalRef: BsModalRef; - - // Normally private - public was needed for the tests - constructor(public modalService: BsModalService) {} - - openCtrlDriven() { - this.ctrlRef = this.modalService.show(DeletionModalComponent, { - initialState: { - submitAction: this.fakeDeleteController.bind(this), - bodyTemplate: this.ctrlDescription - } - }); - } - - openModalDriven() { - this.modalRef = this.modalService.show(DeletionModalComponent, { - initialState: { - submitActionObservable: this.fakeDelete(), - bodyTemplate: this.modalDescription - } - }); - } - - finish() { - this.finished = [6, 7, 8, 9]; - } - - fakeDelete() { - return (): Observable => { - return new Observable((observer: Subscriber) => { - observableTimer(100).subscribe(() => { - observer.next(this.finish()); - observer.complete(); - }); - }); - }; - } - - fakeDeleteController() { - observableTimer(100).subscribe(() => { - this.finish(); - this.ctrlRef.hide(); - }); - } -} - -describe('DeletionModalComponent', () => { - let mockComponent: MockComponent; - let component: DeletionModalComponent; - let mockFixture: ComponentFixture; - let fixture: ComponentFixture; - - configureTestBed({ - declarations: [MockComponent, DeletionModalComponent], - schemas: [NO_ERRORS_SCHEMA], - imports: [ModalModule.forRoot(), ReactiveFormsModule, MockModule, DirectivesModule], - providers: [BsModalRef] - }); - - beforeEach(() => { - mockFixture = TestBed.createComponent(MockComponent); - mockComponent = mockFixture.componentInstance; - // Mocking the modals as a lot would be left over - spyOn(mockComponent.modalService, 'show').and.callFake((modalComp, config) => { - const ref = new BsModalRef(); - fixture = TestBed.createComponent(DeletionModalComponent); - component = fixture.componentInstance; - if (config.initialState) { - component = Object.assign(component, config.initialState); - } - fixture.detectChanges(); - ref.content = component; - return ref; - }); - mockComponent.openCtrlDriven(); - mockFixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should focus the checkbox form field', () => { - fixture.detectChanges(); - fixture.whenStable().then(() => { - const focused = fixture.debugElement.query(By.css(':focus')); - expect(focused.attributes.id).toBe('confirmation'); - expect(focused.attributes.type).toBe('checkbox'); - const element = document.getElementById('confirmation'); - expect(element === document.activeElement).toBeTruthy(); - }); - }); - - it('should throw an error if no action is defined', () => { - component = Object.assign(component, { - submitAction: null, - submitActionObservable: null - }); - expect(() => component.ngOnInit()).toThrowError('No submit action defined'); - }); - - it('should test if the ctrl driven mock is set correctly through mock component', () => { - expect(component.bodyTemplate).toBeTruthy(); - expect(component.submitAction).toBeTruthy(); - expect(component.submitActionObservable).not.toBeTruthy(); - }); - - it('should test if the modal driven mock is set correctly through mock component', () => { - mockComponent.openModalDriven(); - expect(component.bodyTemplate).toBeTruthy(); - expect(component.submitActionObservable).toBeTruthy(); - expect(component.submitAction).not.toBeTruthy(); - }); - - describe('component functions', () => { - const changeValue = (value) => { - const ctrl = component.deletionForm.get('confirmation'); - ctrl.setValue(value); - ctrl.markAsDirty(); - ctrl.updateValueAndValidity(); - fixture.detectChanges(); - }; - - it('should test hideModal', () => { - expect(component.modalRef).toBeTruthy(); - expect(component.hideModal).toBeTruthy(); - spyOn(component.modalRef, 'hide').and.callThrough(); - expect(component.modalRef.hide).not.toHaveBeenCalled(); - component.hideModal(); - expect(component.modalRef.hide).toHaveBeenCalled(); - }); - - describe('validate confirmation', () => { - const testValidation = (submitted: boolean, error: string, expected: boolean) => { - expect( - component.deletionForm.showError('confirmation', { submitted: submitted }, error) - ).toBe(expected); - }; - - beforeEach(() => { - component.deletionForm.reset(); - }); - - it('should test empty values', () => { - component.deletionForm.reset(); - testValidation(false, undefined, false); - testValidation(true, 'required', true); - component.deletionForm.reset(); - changeValue(true); - changeValue(false); - testValidation(true, 'required', true); - }); - }); - - describe('deletion call', () => { - beforeEach(() => { - spyOn(component, 'stopLoadingSpinner').and.callThrough(); - spyOn(component, 'hideModal').and.callThrough(); - }); - - describe('Controller driven', () => { - beforeEach(() => { - spyOn(component, 'submitAction').and.callThrough(); - spyOn(mockComponent.ctrlRef, 'hide').and.callThrough(); - }); - - it('should test fake deletion that closes modal', fakeAsync(() => { - // Before deletionCall - expect(component.submitAction).not.toHaveBeenCalled(); - // During deletionCall - component.callSubmitAction(); - expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); - expect(component.hideModal).not.toHaveBeenCalled(); - expect(mockComponent.ctrlRef.hide).not.toHaveBeenCalled(); - expect(component.submitAction).toHaveBeenCalled(); - expect(mockComponent.finished).toBe(undefined); - // After deletionCall - tick(2000); - expect(component.hideModal).not.toHaveBeenCalled(); - expect(mockComponent.ctrlRef.hide).toHaveBeenCalled(); - expect(mockComponent.finished).toEqual([6, 7, 8, 9]); - })); - }); - - describe('Modal driven', () => { - beforeEach(() => { - mockComponent.openModalDriven(); - spyOn(component, 'stopLoadingSpinner').and.callThrough(); - spyOn(component, 'hideModal').and.callThrough(); - spyOn(mockComponent, 'fakeDelete').and.callThrough(); - }); - - it('should delete and close modal', fakeAsync(() => { - // During deletionCall - component.callSubmitAction(); - expect(mockComponent.finished).toBe(undefined); - expect(component.hideModal).not.toHaveBeenCalled(); - // After deletionCall - tick(2000); - expect(mockComponent.finished).toEqual([6, 7, 8, 9]); - expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); - expect(component.hideModal).toHaveBeenCalled(); - })); - }); - }); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.ts deleted file mode 100644 index 4b6679505e1..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { FormControl, Validators } from '@angular/forms'; - -import { BsModalRef } from 'ngx-bootstrap/modal'; -import { Observable } from 'rxjs'; - -import { CdFormGroup } from '../../forms/cd-form-group'; -import { SubmitButtonComponent } from '../submit-button/submit-button.component'; - -@Component({ - selector: 'cd-deletion-modal', - templateUrl: './deletion-modal.component.html', - styleUrls: ['./deletion-modal.component.scss'] -}) -export class DeletionModalComponent implements OnInit { - @ViewChild(SubmitButtonComponent) - submitButton: SubmitButtonComponent; - bodyTemplate: TemplateRef; - bodyContext: any; - submitActionObservable: () => Observable; - submitAction: Function; - deletionForm: CdFormGroup; - itemDescription: 'entry'; - actionDescription = 'delete'; - - constructor(public modalRef: BsModalRef) {} - - ngOnInit() { - this.deletionForm = new CdFormGroup({ - confirmation: new FormControl(false, [Validators.requiredTrue]) - }); - - if (!(this.submitAction || this.submitActionObservable)) { - throw new Error('No submit action defined'); - } - } - - callSubmitAction() { - if (this.submitActionObservable) { - this.submitActionObservable().subscribe( - null, - this.stopLoadingSpinner.bind(this), - this.hideModal.bind(this) - ); - } else { - this.submitAction(); - } - } - - hideModal() { - this.modalRef.hide(); - } - - stopLoadingSpinner() { - this.deletionForm.setErrors({ cdSubmitButton: true }); - } -}