From 1ae1aa249cfbfcb00c7202f5aab98e29356bbcad Mon Sep 17 00:00:00 2001 From: Tiago Melo Date: Fri, 31 Aug 2018 16:44:12 +0100 Subject: [PATCH] mgr/dashboard: Add UI for RBD Trash Restore Signed-off-by: Tiago Melo --- .../src/app/ceph/block/block.module.ts | 11 ++- .../rbd-trash-list.component.html | 7 ++ .../rbd-trash-list.component.ts | 30 +++++++- .../rbd-trash-restore-modal.component.html | 53 +++++++++++++ .../rbd-trash-restore-modal.component.scss | 0 .../rbd-trash-restore-modal.component.spec.ts | 75 +++++++++++++++++++ .../rbd-trash-restore-modal.component.ts | 62 +++++++++++++++ .../src/app/shared/api/rbd.service.ts | 8 ++ .../shared/services/task-message.service.ts | 9 +++ 9 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts index 8bfd77b00b865..b9c97d6badffd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts @@ -19,9 +19,15 @@ import { RbdSnapshotFormComponent } from './rbd-snapshot-form/rbd-snapshot-form. import { RbdSnapshotListComponent } from './rbd-snapshot-list/rbd-snapshot-list.component'; import { RbdTrashListComponent } from './rbd-trash-list/rbd-trash-list.component'; import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-move-modal.component'; +import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-trash-restore-modal.component'; @NgModule({ - entryComponents: [RbdDetailsComponent, RbdSnapshotFormComponent, RbdTrashMoveModalComponent], + entryComponents: [ + RbdDetailsComponent, + RbdSnapshotFormComponent, + RbdTrashMoveModalComponent, + RbdTrashRestoreModalComponent + ], imports: [ CommonModule, FormsModule, @@ -46,7 +52,8 @@ import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-mov RbdSnapshotFormComponent, RbdTrashListComponent, RbdTrashMoveModalComponent, - RbdImagesComponent + RbdImagesComponent, + RbdTrashRestoreModalComponent ] }) export class BlockModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html index 4933b09848897..51a2be256be9b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html @@ -9,6 +9,13 @@ forceIdentifier="true" selectionType="single" (updateSelection)="updateSelection($event)"> +
+ + +
this.restoreModal(), + name: 'Restore' + }; + this.tableActions = [restoreAction]; + } ngOnInit() { this.columns = [ @@ -129,4 +146,15 @@ export class RbdTrashListComponent implements OnInit { updateSelection(selection: CdTableSelection) { this.selection = selection; } + + restoreModal() { + const initialState = { + metaType: 'RBD', + poolName: this.selection.first().pool_name, + imageName: this.selection.first().name, + imageId: this.selection.first().id + }; + + this.modalRef = this.modalService.show(RbdTrashRestoreModalComponent, { initialState }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html new file mode 100644 index 0000000000000..42527905d652a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html @@ -0,0 +1,53 @@ + + Restore Image + + +
+ + + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.spec.ts new file mode 100644 index 0000000000000..91c5e83642600 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.spec.ts @@ -0,0 +1,75 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastModule } from 'ng2-toastr'; +import { BsModalRef } from 'ngx-bootstrap'; + +import { configureTestBed } from '../../../../testing/unit-test-helper'; +import { NotificationService } from '../../../shared/services/notification.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal.component'; + +describe('RbdTrashRestoreModalComponent', () => { + let component: RbdTrashRestoreModalComponent; + let fixture: ComponentFixture; + + configureTestBed({ + declarations: [RbdTrashRestoreModalComponent], + imports: [ + ReactiveFormsModule, + HttpClientTestingModule, + ToastModule.forRoot(), + SharedModule, + RouterTestingModule + ], + providers: [BsModalRef] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RbdTrashRestoreModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('should call restore', () => { + let httpTesting: HttpTestingController; + let notificationService: NotificationService; + let modalRef: BsModalRef; + let req; + + beforeEach(() => { + httpTesting = TestBed.get(HttpTestingController); + notificationService = TestBed.get(NotificationService); + modalRef = TestBed.get(BsModalRef); + + component.poolName = 'foo'; + component.imageId = 'bar'; + + spyOn(modalRef, 'hide').and.stub(); + spyOn(component.restoreForm, 'setErrors').and.stub(); + spyOn(notificationService, 'show').and.stub(); + + component.restore(); + + req = httpTesting.expectOne('api/block/image/trash/foo/bar/restore'); + }); + + it('with success', () => { + req.flush(null); + expect(component.restoreForm.setErrors).toHaveBeenCalledTimes(0); + expect(component.modalRef.hide).toHaveBeenCalledTimes(1); + }); + + it('with failure', () => { + req.flush(null, { status: 500, statusText: 'failure' }); + expect(component.restoreForm.setErrors).toHaveBeenCalledTimes(1); + expect(component.modalRef.hide).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.ts new file mode 100644 index 0000000000000..b1d0ff6953fe9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.ts @@ -0,0 +1,62 @@ +import { Component, OnInit } from '@angular/core'; + +import { BsModalRef } from 'ngx-bootstrap'; + +import { RbdService } from '../../../shared/api/rbd.service'; +import { CdFormBuilder } from '../../../shared/forms/cd-form-builder'; +import { CdFormGroup } from '../../../shared/forms/cd-form-group'; +import { ExecutingTask } from '../../../shared/models/executing-task'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; + +@Component({ + selector: 'cd-rbd-trash-restore-modal', + templateUrl: './rbd-trash-restore-modal.component.html', + styleUrls: ['./rbd-trash-restore-modal.component.scss'] +}) +export class RbdTrashRestoreModalComponent implements OnInit { + metaType: string; + poolName: string; + imageName: string; + imageId: string; + executingTasks: ExecutingTask[]; + + restoreForm: CdFormGroup; + + constructor( + private rbdService: RbdService, + public modalRef: BsModalRef, + private fb: CdFormBuilder, + private taskWrapper: TaskWrapperService + ) {} + + ngOnInit() { + this.restoreForm = this.fb.group({ + name: this.imageName + }); + } + + restore() { + const name = this.restoreForm.getValue('name'); + + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('rbd/trash/restore', { + pool_name: this.poolName, + image_id: this.imageId, + image_name: this.imageName, + new_image_name: name + }), + call: this.rbdService.restoreTrash(this.poolName, this.imageId, name) + }) + .subscribe( + undefined, + () => { + this.restoreForm.setErrors({ cdSubmitButton: true }); + }, + () => { + this.modalRef.hide(); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts index 2eba24eb76c10..97efc6d331e35 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts @@ -107,4 +107,12 @@ export class RbdService { { observe: 'response' } ); } + + restoreTrash(poolName, imageId, newImageName) { + return this.http.post( + `api/block/image/trash/${poolName}/${imageId}/restore`, + { new_image_name: newImageName }, + { observe: 'response' } + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 4da4c6c5dd2ba..45f9a07b9c962 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -133,6 +133,15 @@ export class TaskMessageService { () => ({ 2: `Could not find image.` }) + ), + 'rbd/trash/restore': new TaskMessage( + new TaskMessageOperation('Restoring', 'restore', 'Restored'), + (metadata) => + `image '${metadata.pool_name}/${metadata.image_name}@${metadata.image_id}' \ + into '${metadata.pool_name}/${metadata.new_image_name}'`, + (metadata) => ({ + 17: `Image name '${metadata.pool_name}/${metadata.new_image_name}' is already in use.` + }) ) }; -- 2.39.5