From: Stephan Müller Date: Mon, 16 Apr 2018 08:57:31 +0000 (+0200) Subject: mgr/dashboard: Changes deletion button to link X-Git-Tag: v13.1.0~126^2~2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=f2096e9378b84ba437b15a5537c2ead73c38f0eb;p=ceph.git mgr/dashboard: Changes deletion button to link This change was made because a link can be placed anywhere instead of a button element. Signed-off-by: Stephan Müller --- 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 c716d9a5befb..cc3dd5bbfebe 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 @@ -9,7 +9,7 @@ import { PipesModule } from '../pipes/pipes.module'; import { DeleteConfirmationComponent } from './delete-confirmation-modal/delete-confirmation-modal.component'; -import { DeletionButtonComponent } from './deletion-button/deletion-button.component'; +import { DeletionLinkComponent } from './deletion-link/deletion-link.component'; import { HelperComponent } from './helper/helper.component'; import { ModalComponent } from './modal/modal.component'; import { SparklineComponent } from './sparkline/sparkline.component'; @@ -38,7 +38,7 @@ import { ViewCacheComponent } from './view-cache/view-cache.component'; UsageBarComponent, DeleteConfirmationComponent, ModalComponent, - DeletionButtonComponent + DeletionLinkComponent ], providers: [], exports: [ @@ -52,7 +52,7 @@ import { ViewCacheComponent } from './view-cache/view-cache.component'; entryComponents: [ DeleteConfirmationComponent, ModalComponent, - DeletionButtonComponent + DeletionLinkComponent ] }) export class ComponentsModule { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.html deleted file mode 100644 index 8744e4b345c7..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - -
- - -
-
- - - - Delete - - {{ metaType }} - - - - - - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.spec.ts deleted file mode 100644 index 564a0e2b9857..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.spec.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { FormGroupDirective, ReactiveFormsModule } from '@angular/forms'; - -import { ModalModule } from 'ngx-bootstrap'; -import { Observable } from 'rxjs/Observable'; -import { Subscriber } from 'rxjs/Subscriber'; - -import { ModalComponent } from '../modal/modal.component'; -import { SubmitButtonComponent } from '../submit-button/submit-button.component'; -import { DeletionButtonComponent } from './deletion-button.component'; - -@Component({ - template: ` - - The spinner is handled by the controller if you have use the modal as ViewChild in order to - use it's functions to stop the spinner or close the dialog. - - - The spinner is handled by the modal if your given deletion function returns a Observable. - - ` -}) -class MockComponent { - @ViewChild('ctrlDeleteButton') ctrlDeleteButton: DeletionButtonComponent; - @ViewChild('modalDeleteButton') modalDeleteButton: DeletionButtonComponent; - someData = [1, 2, 3, 4, 5]; - finished: number[]; - - finish() { - this.finished = [6, 7, 8, 9]; - } - - fakeDelete() { - return (): Observable => { - return new Observable((observer: Subscriber) => { - Observable.timer(100).subscribe(() => { - observer.next(this.finish()); - observer.complete(); - }); - }); - }; - } - - fakeDeleteController() { - Observable.timer(100).subscribe(() => { - this.finish(); - this.ctrlDeleteButton.hideModal(); - }); - } -} - -describe('DeletionButtonComponent', () => { - let mockComponent: MockComponent; - let component: DeletionButtonComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ MockComponent, DeletionButtonComponent, ModalComponent, - SubmitButtonComponent], - imports: [ModalModule.forRoot(), ReactiveFormsModule], - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(MockComponent); - mockComponent = fixture.componentInstance; - component = mockComponent.ctrlDeleteButton; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('component functions', () => { - - const mockShowModal = () => { - component.showModal(null); - }; - - const changeValue = (value) => { - component.confirmation.setValue(value); - component.confirmation.markAsDirty(); - component.confirmation.updateValueAndValidity(); - }; - - beforeEach(() => { - spyOn(component.modalService, 'show').and.returnValue({ - hide: () => true - }); - }); - - it('should test showModal', () => { - changeValue('something'); - expect(mockShowModal).toBeTruthy(); - expect(component.confirmation.value).toBe('something'); - expect(component.modalService.show).not.toHaveBeenCalled(); - mockShowModal(); - expect(component.modalService.show).toHaveBeenCalled(); - expect(component.confirmation.value).toBe(null); - expect(component.confirmation.pristine).toBe(true); - }); - - it('should test hideModal', () => { - expect(component.bsModalRef).not.toBeTruthy(); - mockShowModal(); - expect(component.bsModalRef).toBeTruthy(); - expect(component.hideModal).toBeTruthy(); - spyOn(component.bsModalRef, 'hide').and.stub(); - expect(component.bsModalRef.hide).not.toHaveBeenCalled(); - component.hideModal(); - expect(component.bsModalRef.hide).toHaveBeenCalled(); - }); - - describe('invalid control', () => { - - const testInvalidControl = (submitted: boolean, error: string, expected: boolean) => { - expect(component.invalidControl(submitted, error)).toBe(expected); - }; - - beforeEach(() => { - component.deletionForm.reset(); - }); - - it('should test empty values', () => { - expect(component.invalidControl).toBeTruthy(); - component.deletionForm.reset(); - testInvalidControl(false, undefined, false); - testInvalidControl(true, 'required', true); - component.deletionForm.reset(); - changeValue('let-me-pass'); - changeValue(''); - testInvalidControl(true, 'required', true); - }); - - it('should test pattern', () => { - changeValue('let-me-pass'); - testInvalidControl(false, 'pattern', true); - changeValue('ctrl-test'); - testInvalidControl(false, undefined, false); - testInvalidControl(true, undefined, false); - }); - }); - - describe('deletion call', () => { - beforeEach(() => { - spyOn(component.toggleDeletion, 'emit'); - spyOn(component, 'stopLoadingSpinner'); - spyOn(component, 'hideModal').and.stub(); - }); - - describe('Controller driven', () => { - beforeEach(() => { - mockShowModal(); - expect(component.toggleDeletion.emit).not.toHaveBeenCalled(); - expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); - expect(component.hideModal).not.toHaveBeenCalled(); - }); - - it('should delete without doing anything but call emit', () => { - component.deletionCall(); - expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); - expect(component.hideModal).not.toHaveBeenCalled(); - expect(component.toggleDeletion.emit).toHaveBeenCalled(); - }); - - it('should test fake deletion that closes modal', fakeAsync(() => { - mockComponent.fakeDeleteController(); - expect(component.hideModal).not.toHaveBeenCalled(); - expect(mockComponent.finished).toBe(undefined); - tick(2000); - expect(component.hideModal).toHaveBeenCalled(); - expect(mockComponent.finished).toEqual([6, 7, 8, 9]); - })); - }); - - describe('Modal driven', () => { - it('should delete and close modal', fakeAsync(() => { - component = mockComponent.modalDeleteButton; - mockShowModal(); - spyOn(component.toggleDeletion, 'emit'); - spyOn(component, 'stopLoadingSpinner'); - spyOn(component, 'hideModal').and.stub(); - spyOn(mockComponent, 'fakeDelete'); - - component.deletionCall(); - expect(mockComponent.finished).toBe(undefined); - expect(component.toggleDeletion.emit).not.toHaveBeenCalled(); - expect(component.hideModal).not.toHaveBeenCalled(); - - tick(2000); - expect(component.toggleDeletion.emit).not.toHaveBeenCalled(); - expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); - expect(component.hideModal).toHaveBeenCalled(); - expect(mockComponent.finished).toEqual([6, 7, 8, 9]); - })); - }); - }); - }); - -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.ts deleted file mode 100644 index 407e411e92d7..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-button/deletion-button.component.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { - Component, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild -} from '@angular/core'; -import { FormControl, FormGroup, FormGroupDirective, Validators } from '@angular/forms'; - -import { BsModalRef, BsModalService } from 'ngx-bootstrap'; -import { Observable } from 'rxjs/Observable'; - -import { SubmitButtonComponent } from '../submit-button/submit-button.component'; - -@Component({ - selector: 'cd-deletion-button', - templateUrl: './deletion-button.component.html', - styleUrls: ['./deletion-button.component.scss'] -}) -export class DeletionButtonComponent implements OnInit { - @ViewChild(SubmitButtonComponent) submitButton: SubmitButtonComponent; - @Input() metaType: string; - @Input() pattern = 'yes'; - @Input() btnClasses = 'btn btn-sm btn-primary'; - @Input() deletionObserver: () => Observable; - @Output() toggleDeletion = new EventEmitter(); - bsModalRef: BsModalRef; - deletionForm: FormGroup; - confirmation: FormControl; - delete: Function; - - constructor(public modalService: BsModalService) {} - - ngOnInit() { - this.confirmation = new FormControl('', { - validators: [ - Validators.required, - Validators.pattern(this.pattern) - ], - updateOn: 'blur' - }); - this.deletionForm = new FormGroup({ - confirmation: this.confirmation - }); - } - - showModal(template: TemplateRef) { - this.deletionForm.reset(); - this.bsModalRef = this.modalService.show(template); - this.delete = () => { - this.submitButton.submit(); - }; - } - - invalidControl(submitted: boolean, error?: string): boolean { - const control = this.confirmation; - return !!( - (submitted || control.dirty) && - control.invalid && - (error ? control.errors[error] : true) - ); - } - - updateConfirmation($e) { - if ($e.key !== 'Enter') { - return; - } - this.confirmation.setValue($e.target.value); - this.confirmation.markAsDirty(); - this.confirmation.updateValueAndValidity(); - } - - deletionCall() { - if (this.deletionObserver) { - this.deletionObserver().subscribe( - undefined, - () => this.stopLoadingSpinner(), - () => this.hideModal() - ); - } else { - this.toggleDeletion.emit(); - } - } - - hideModal() { - this.bsModalRef.hide(); - } - - stopLoadingSpinner() { - this.submitButton.loading = false; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.html new file mode 100644 index 000000000000..9910fe9d7b3c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.html @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + Delete + + {{ metaType }} + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.spec.ts new file mode 100644 index 000000000000..9ee3f608ba0f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.spec.ts @@ -0,0 +1,210 @@ +import { Component, ViewChild } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { FormGroupDirective, ReactiveFormsModule } from '@angular/forms'; + +import { ModalModule } from 'ngx-bootstrap'; +import { Observable } from 'rxjs/Observable'; +import { Subscriber } from 'rxjs/Subscriber'; + +import { ModalComponent } from '../modal/modal.component'; +import { SubmitButtonComponent } from '../submit-button/submit-button.component'; +import { DeletionLinkComponent } from './deletion-link.component'; + +@Component({ + template: ` + + The spinner is handled by the controller if you have use the modal as ViewChild in order to + use it's functions to stop the spinner or close the dialog. + + + The spinner is handled by the modal if your given deletion function returns a Observable. + + ` +}) +class MockComponent { + @ViewChild('ctrlDeleteButton') ctrlDeleteButton: DeletionLinkComponent; + @ViewChild('modalDeleteButton') modalDeleteButton: DeletionLinkComponent; + someData = [1, 2, 3, 4, 5]; + finished: number[]; + + finish() { + this.finished = [6, 7, 8, 9]; + } + + fakeDelete() { + return (): Observable => { + return new Observable((observer: Subscriber) => { + Observable.timer(100).subscribe(() => { + observer.next(this.finish()); + observer.complete(); + }); + }); + }; + } + + fakeDeleteController() { + Observable.timer(100).subscribe(() => { + this.finish(); + this.ctrlDeleteButton.hideModal(); + }); + } +} + +describe('DeletionLinkComponent', () => { + let mockComponent: MockComponent; + let component: DeletionLinkComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MockComponent, DeletionLinkComponent, ModalComponent, + SubmitButtonComponent], + imports: [ModalModule.forRoot(), ReactiveFormsModule], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MockComponent); + mockComponent = fixture.componentInstance; + component = mockComponent.ctrlDeleteButton; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('component functions', () => { + + const mockShowModal = () => { + component.showModal(null); + }; + + const changeValue = (value) => { + component.confirmation.setValue(value); + component.confirmation.markAsDirty(); + component.confirmation.updateValueAndValidity(); + }; + + beforeEach(() => { + spyOn(component.modalService, 'show').and.returnValue({ + hide: () => true + }); + }); + + it('should test showModal', () => { + changeValue('something'); + expect(mockShowModal).toBeTruthy(); + expect(component.confirmation.value).toBe('something'); + expect(component.modalService.show).not.toHaveBeenCalled(); + mockShowModal(); + expect(component.modalService.show).toHaveBeenCalled(); + expect(component.confirmation.value).toBe(null); + expect(component.confirmation.pristine).toBe(true); + }); + + it('should test hideModal', () => { + expect(component.bsModalRef).not.toBeTruthy(); + mockShowModal(); + expect(component.bsModalRef).toBeTruthy(); + expect(component.hideModal).toBeTruthy(); + spyOn(component.bsModalRef, 'hide').and.stub(); + expect(component.bsModalRef.hide).not.toHaveBeenCalled(); + component.hideModal(); + expect(component.bsModalRef.hide).toHaveBeenCalled(); + }); + + describe('invalid control', () => { + + const testInvalidControl = (submitted: boolean, error: string, expected: boolean) => { + expect(component.invalidControl(submitted, error)).toBe(expected); + }; + + beforeEach(() => { + component.deletionForm.reset(); + }); + + it('should test empty values', () => { + expect(component.invalidControl).toBeTruthy(); + component.deletionForm.reset(); + testInvalidControl(false, undefined, false); + testInvalidControl(true, 'required', true); + component.deletionForm.reset(); + changeValue('let-me-pass'); + changeValue(''); + testInvalidControl(true, 'required', true); + }); + + it('should test pattern', () => { + changeValue('let-me-pass'); + testInvalidControl(false, 'pattern', true); + changeValue('ctrl-test'); + testInvalidControl(false, undefined, false); + testInvalidControl(true, undefined, false); + }); + }); + + describe('deletion call', () => { + beforeEach(() => { + spyOn(component.toggleDeletion, 'emit'); + spyOn(component, 'stopLoadingSpinner'); + spyOn(component, 'hideModal').and.stub(); + }); + + describe('Controller driven', () => { + beforeEach(() => { + mockShowModal(); + expect(component.toggleDeletion.emit).not.toHaveBeenCalled(); + expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); + expect(component.hideModal).not.toHaveBeenCalled(); + }); + + it('should delete without doing anything but call emit', () => { + component.deletionCall(); + expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); + expect(component.hideModal).not.toHaveBeenCalled(); + expect(component.toggleDeletion.emit).toHaveBeenCalled(); + }); + + it('should test fake deletion that closes modal', fakeAsync(() => { + mockComponent.fakeDeleteController(); + expect(component.hideModal).not.toHaveBeenCalled(); + expect(mockComponent.finished).toBe(undefined); + tick(2000); + expect(component.hideModal).toHaveBeenCalled(); + expect(mockComponent.finished).toEqual([6, 7, 8, 9]); + })); + }); + + describe('Modal driven', () => { + it('should delete and close modal', fakeAsync(() => { + component = mockComponent.modalDeleteButton; + mockShowModal(); + spyOn(component.toggleDeletion, 'emit'); + spyOn(component, 'stopLoadingSpinner'); + spyOn(component, 'hideModal').and.stub(); + spyOn(mockComponent, 'fakeDelete'); + + component.deletionCall(); + expect(mockComponent.finished).toBe(undefined); + expect(component.toggleDeletion.emit).not.toHaveBeenCalled(); + expect(component.hideModal).not.toHaveBeenCalled(); + + tick(2000); + expect(component.toggleDeletion.emit).not.toHaveBeenCalled(); + expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); + expect(component.hideModal).toHaveBeenCalled(); + expect(mockComponent.finished).toEqual([6, 7, 8, 9]); + })); + }); + }); + }); + +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.ts new file mode 100644 index 000000000000..cd79da24fc0d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.ts @@ -0,0 +1,87 @@ +import { + Component, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild +} from '@angular/core'; +import { FormControl, FormGroup, FormGroupDirective, Validators } from '@angular/forms'; + +import { BsModalRef, BsModalService } from 'ngx-bootstrap'; +import { Observable } from 'rxjs/Observable'; + +import { SubmitButtonComponent } from '../submit-button/submit-button.component'; + +@Component({ + selector: 'cd-deletion-link', + templateUrl: './deletion-link.component.html', + styleUrls: ['./deletion-link.component.scss'] +}) +export class DeletionLinkComponent implements OnInit { + @ViewChild(SubmitButtonComponent) submitButton: SubmitButtonComponent; + @Input() metaType: string; + @Input() pattern = 'yes'; + @Input() deletionObserver: () => Observable; + @Output() toggleDeletion = new EventEmitter(); + bsModalRef: BsModalRef; + deletionForm: FormGroup; + confirmation: FormControl; + delete: Function; + + constructor(public modalService: BsModalService) {} + + ngOnInit() { + this.confirmation = new FormControl('', { + validators: [ + Validators.required, + Validators.pattern(this.pattern) + ], + updateOn: 'blur' + }); + this.deletionForm = new FormGroup({ + confirmation: this.confirmation + }); + } + + showModal(template: TemplateRef) { + this.deletionForm.reset(); + this.bsModalRef = this.modalService.show(template); + this.delete = () => { + this.submitButton.submit(); + }; + } + + invalidControl(submitted: boolean, error?: string): boolean { + const control = this.confirmation; + return !!( + (submitted || control.dirty) && + control.invalid && + (error ? control.errors[error] : true) + ); + } + + updateConfirmation($e) { + if ($e.key !== 'Enter') { + return; + } + this.confirmation.setValue($e.target.value); + this.confirmation.markAsDirty(); + this.confirmation.updateValueAndValidity(); + } + + deletionCall() { + if (this.deletionObserver) { + this.deletionObserver().subscribe( + undefined, + () => this.stopLoadingSpinner(), + () => this.hideModal() + ); + } else { + this.toggleDeletion.emit(); + } + } + + hideModal() { + this.bsModalRef.hide(); + } + + stopLoadingSpinner() { + this.submitButton.loading = false; + } +}