]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add UI for RBD Trash Restore
authorTiago Melo <tspmelo@gmail.com>
Fri, 31 Aug 2018 15:44:12 +0000 (16:44 +0100)
committerTiago Melo <tspmelo@gmail.com>
Tue, 25 Sep 2018 13:02:58 +0000 (14:02 +0100)
Signed-off-by: Tiago Melo <tmelo@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts

index 8bfd77b00b865d2e9fbe3d73af470ba7d2e4cb09..b9c97d6badffd402a297bb2ae4739fc72712f66d 100644 (file)
@@ -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 {}
index 4933b09848897ca24c6369d0ea1b6a5e37dde516..51a2be256be9b441730acf9c489970636ba444ae 100644 (file)
@@ -9,6 +9,13 @@
           forceIdentifier="true"
           selectionType="single"
           (updateSelection)="updateSelection($event)">
+  <div class="table-actions btn-toolbar">
+    <cd-table-actions class="btn-group"
+                      [permission]="permission"
+                      [selection]="selection"
+                      [tableActions]="tableActions">
+    </cd-table-actions>
+  </div>
 </cd-table>
 
 <ng-template #expiresTpl
index 8dbfb5f2a81a9b7b949cc284c778f21c66d3dbf0..e4474dd2c22a9e4e5177a52a65e821035482add7 100644 (file)
@@ -8,11 +8,15 @@ import { RbdService } from '../../../shared/api/rbd.service';
 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';
+import { CdTableAction } from '../../../shared/models/cd-table-action';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { ExecutingTask } from '../../../shared/models/executing-task';
+import { Permission } from '../../../shared/models/permissions';
 import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
 import { TaskListService } from '../../../shared/services/task-list.service';
+import { RbdTrashRestoreModalComponent } from '../rbd-trash-restore-modal/rbd-trash-restore-modal.component';
 
 @Component({
   selector: 'cd-rbd-trash-list',
@@ -30,16 +34,29 @@ export class RbdTrashListComponent implements OnInit {
   executingTasks: ExecutingTask[] = [];
   images: any;
   modalRef: BsModalRef;
+  permission: Permission;
   retries: number;
   selection = new CdTableSelection();
+  tableActions: CdTableAction[];
   viewCacheStatusList: any[];
 
   constructor(
+    private authStorageService: AuthStorageService,
     private rbdService: RbdService,
     private modalService: BsModalService,
     private cdDatePipe: CdDatePipe,
     private taskListService: TaskListService
-  ) {}
+  ) {
+    this.permission = this.authStorageService.getPermissions().rbdImage;
+
+    const restoreAction: CdTableAction = {
+      permission: 'update',
+      icon: 'fa-undo',
+      click: () => 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 (file)
index 0000000..4252790
--- /dev/null
@@ -0,0 +1,53 @@
+<cd-modal>
+  <ng-container i18n
+                class="modal-title">Restore Image</ng-container>
+
+  <ng-container class="modal-content">
+    <form name="restoreForm"
+          class="form"
+          #formDir="ngForm"
+          [formGroup]="restoreForm"
+          novalidate>
+      <div class="modal-body">
+        <p>
+          <ng-container i18n>To restore</ng-container>&nbsp;
+          <kbd>{{ poolName }}/{{ imageName }}@{{ imageId }}</kbd>,&nbsp;
+          <ng-container i18n>type the image's new name and click</ng-container>&nbsp;
+          <kbd i18n>Restore Image</kbd>.
+        </p>
+
+        <div class="form-group"
+             [ngClass]="{'has-error': restoreForm.showError('name', formDir)}">
+          <label for="name"
+                 i18n>New Name</label>
+          <input type="text"
+                 class="form-control"
+                 name="name"
+                 id="name"
+                 autocomplete="off"
+                 formControlName="name"
+                 autofocus>
+          <span class="help-block"
+                *ngIf="restoreForm.showError('name', formDir, 'required')"
+                i18n>
+            This field is required.
+          </span>
+        </div>
+      </div>
+
+      <div class="modal-footer">
+        <div class="button-group text-right">
+          <cd-submit-button i18n
+                            [form]="restoreForm"
+                            (submitAction)="restore()">
+            Restore Image
+          </cd-submit-button>
+          <button i18n
+                  type="button"
+                  class="btn btn-sm btn-default"
+                  (click)="modalRef.hide()">Cancel</button>
+        </div>
+      </div>
+    </form>
+  </ng-container>
+</cd-modal>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..91c5e83
--- /dev/null
@@ -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<RbdTrashRestoreModalComponent>;
+
+  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 (file)
index 0000000..b1d0ff6
--- /dev/null
@@ -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();
+        }
+      );
+  }
+}
index 2eba24eb76c10997e59a8bdf6cfb96813d6334ae..97efc6d331e359c797a9b1cdf863c55c82437ffc 100644 (file)
@@ -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' }
+    );
+  }
 }
index 4da4c6c5dd2ba1cde89c9a68cb01c525db0774a4..45f9a07b9c9622f14f782cb383214250878a1ffe 100644 (file)
@@ -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.`
+      })
     )
   };