]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add UI for RBD Trash List
authorTiago Melo <tmelo@suse.com>
Tue, 19 Jun 2018 14:09:23 +0000 (15:09 +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>
12 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.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-list.service.ts

index 5de3f59300082d6006f3ef67c505286bceb58a53..efe4ed58db89d92be2fee9679d0dc8cf63d60fc1 100644 (file)
@@ -4,7 +4,7 @@ import { ActivatedRouteSnapshot, RouterModule, Routes } from '@angular/router';
 import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
 import { MirroringComponent } from './ceph/block/mirroring/mirroring.component';
 import { RbdFormComponent } from './ceph/block/rbd-form/rbd-form.component';
-import { RbdListComponent } from './ceph/block/rbd-list/rbd-list.component';
+import { RbdImagesComponent } from './ceph/block/rbd-images/rbd-images.component';
 import { CephfsListComponent } from './ceph/cephfs/cephfs-list/cephfs-list.component';
 import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
 import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
@@ -107,7 +107,7 @@ const routes: Routes = [
         path: 'rbd',
         data: { breadcrumbs: 'Images' },
         children: [
-          { path: '', component: RbdListComponent },
+          { path: '', component: RbdImagesComponent },
           { path: 'add', component: RbdFormComponent, data: { breadcrumbs: 'Add' } },
           { path: 'edit/:pool/:name', component: RbdFormComponent, data: { breadcrumbs: 'Edit' } },
           {
index e6300ee09fec9cf206cf5fc5d7ee007e4147d580..8bfd77b00b865d2e9fbe3d73af470ba7d2e4cb09 100644 (file)
@@ -13,17 +13,15 @@ import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
 import { MirroringComponent } from './mirroring/mirroring.component';
 import { RbdDetailsComponent } from './rbd-details/rbd-details.component';
 import { RbdFormComponent } from './rbd-form/rbd-form.component';
+import { RbdImagesComponent } from './rbd-images/rbd-images.component';
 import { RbdListComponent } from './rbd-list/rbd-list.component';
 import { RbdSnapshotFormComponent } from './rbd-snapshot-form/rbd-snapshot-form.component';
 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';
 
 @NgModule({
-  entryComponents: [
-    RbdDetailsComponent,
-    RbdSnapshotFormComponent,
-    RbdTrashMoveModalComponent
-  ],
+  entryComponents: [RbdDetailsComponent, RbdSnapshotFormComponent, RbdTrashMoveModalComponent],
   imports: [
     CommonModule,
     FormsModule,
@@ -46,7 +44,9 @@ import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-mov
     RbdFormComponent,
     RbdSnapshotListComponent,
     RbdSnapshotFormComponent,
-    RbdTrashMoveModalComponent
+    RbdTrashListComponent,
+    RbdTrashMoveModalComponent,
+    RbdImagesComponent
   ]
 })
 export class BlockModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.html
new file mode 100644 (file)
index 0000000..f629bd7
--- /dev/null
@@ -0,0 +1,13 @@
+<div>
+  <tabset>
+    <tab heading="Images"
+         i18n-heading
+         id="tab1">
+      <cd-rbd-list></cd-rbd-list>
+    </tab>
+    <tab heading="Trash"
+         i18n-heading>
+      <cd-rbd-trash-list></cd-rbd-trash-list>
+    </tab>
+  </tabset>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.spec.ts
new file mode 100644 (file)
index 0000000..82cc3f7
--- /dev/null
@@ -0,0 +1,50 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+import { TabsModule, TooltipModule } from 'ngx-bootstrap';
+
+import { TaskListService } from '../../../shared/services/task-list.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { RbdDetailsComponent } from '../rbd-details/rbd-details.component';
+import { RbdListComponent } from '../rbd-list/rbd-list.component';
+import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
+import { RbdTrashListComponent } from '../rbd-trash-list/rbd-trash-list.component';
+import { RbdImagesComponent } from './rbd-images.component';
+
+describe('RbdImagesComponent', () => {
+  let component: RbdImagesComponent;
+  let fixture: ComponentFixture<RbdImagesComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [
+        RbdDetailsComponent,
+        RbdImagesComponent,
+        RbdListComponent,
+        RbdSnapshotListComponent,
+        RbdTrashListComponent
+      ],
+      imports: [
+        HttpClientTestingModule,
+        RouterTestingModule,
+        SharedModule,
+        TabsModule.forRoot(),
+        ToastModule.forRoot(),
+        TooltipModule.forRoot()
+      ],
+      providers: [TaskListService]
+    }).compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RbdImagesComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.ts
new file mode 100644 (file)
index 0000000..78e78ff
--- /dev/null
@@ -0,0 +1,12 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'cd-rbd-images',
+  templateUrl: './rbd-images.component.html',
+  styleUrls: ['./rbd-images.component.scss']
+})
+export class RbdImagesComponent implements OnInit {
+  constructor() {}
+
+  ngOnInit() {}
+}
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
new file mode 100644 (file)
index 0000000..4933b09
--- /dev/null
@@ -0,0 +1,26 @@
+<cd-view-cache *ngFor="let viewCacheStatus of viewCacheStatusList"
+               [status]="viewCacheStatus.status"
+               [statusFor]="viewCacheStatus.statusFor"></cd-view-cache>
+
+<cd-table [data]="images"
+          columnMode="flex"
+          [columns]="columns"
+          identifier="id"
+          forceIdentifier="true"
+          selectionType="single"
+          (updateSelection)="updateSelection($event)">
+</cd-table>
+
+<ng-template #expiresTpl
+             let-row="row"
+             let-value="value">
+  <ng-container *ngIf="row.cdIsExpired"
+                i18n>Expired at
+  </ng-container>
+
+  <ng-container *ngIf="!row.cdIsExpired"
+                i18n>Protected until
+  </ng-container>
+
+  {{ value | cdDate }}
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts
new file mode 100644 (file)
index 0000000..1bb18d2
--- /dev/null
@@ -0,0 +1,97 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+import { of } from 'rxjs';
+
+import { configureTestBed } from '../../../../testing/unit-test-helper';
+import { RbdService } from '../../../shared/api/rbd.service';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { ExecutingTask } from '../../../shared/models/executing-task';
+import { SummaryService } from '../../../shared/services/summary.service';
+import { TaskListService } from '../../../shared/services/task-list.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { RbdTrashListComponent } from './rbd-trash-list.component';
+
+describe('RbdTrashListComponent', () => {
+  let component: RbdTrashListComponent;
+  let fixture: ComponentFixture<RbdTrashListComponent>;
+  let summaryService: SummaryService;
+  let rbdService: RbdService;
+
+  configureTestBed({
+    declarations: [RbdTrashListComponent],
+    imports: [SharedModule, HttpClientTestingModule, RouterTestingModule, ToastModule.forRoot()],
+    providers: [TaskListService, RbdService]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RbdTrashListComponent);
+    component = fixture.componentInstance;
+    summaryService = TestBed.get(SummaryService);
+    rbdService = TestBed.get(RbdService);
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should load trash images when summary is trigged', () => {
+    spyOn(rbdService, 'listTrash').and.callThrough();
+
+    summaryService['summaryDataSource'].next({ executingTasks: null });
+    expect(rbdService.listTrash).toHaveBeenCalled();
+  });
+
+  it('should call updateSelection', () => {
+    const selection = new CdTableSelection();
+    selection.selected = ['foo'];
+    selection.update();
+
+    expect(component.selection.hasSelection).toBeFalsy();
+    component.updateSelection(selection);
+    expect(component.selection.hasSelection).toBeTruthy();
+  });
+
+  describe('handling of executing tasks', () => {
+    let images: any[];
+
+    const addImage = (id) => {
+      images.push({
+        id: id
+      });
+    };
+
+    const addTask = (name: string, image_id: string) => {
+      const task = new ExecutingTask();
+      task.name = name;
+      task.metadata = {
+        image_id: image_id
+      };
+      summaryService.addRunningTask(task);
+    };
+
+    const expectImageTasks = (image: any, executing: string) => {
+      expect(image.cdExecuting).toEqual(executing);
+    };
+
+    beforeEach(() => {
+      images = [];
+      addImage('1');
+      addImage('2');
+      component.images = images;
+      summaryService['summaryDataSource'].next({ executingTasks: [] });
+      spyOn(rbdService, 'listTrash').and.callFake(() =>
+        of([{ poool_name: 'rbd', status: 1, value: images }])
+      );
+      fixture.detectChanges();
+    });
+
+    it('should gets all images without tasks', () => {
+      expect(component.images.length).toBe(2);
+      expect(component.images.every((image) => !image.cdExecuting)).toBeTruthy();
+    });
+  });
+});
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
new file mode 100644 (file)
index 0000000..8dbfb5f
--- /dev/null
@@ -0,0 +1,132 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import * as _ from 'lodash';
+import * as moment from 'moment';
+import { BsModalRef, BsModalService } from 'ngx-bootstrap';
+
+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 { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { ExecutingTask } from '../../../shared/models/executing-task';
+import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
+import { TaskListService } from '../../../shared/services/task-list.service';
+
+@Component({
+  selector: 'cd-rbd-trash-list',
+  templateUrl: './rbd-trash-list.component.html',
+  styleUrls: ['./rbd-trash-list.component.scss'],
+  providers: [TaskListService]
+})
+export class RbdTrashListComponent implements OnInit {
+  @ViewChild(TableComponent)
+  table: TableComponent;
+  @ViewChild('expiresTpl')
+  expiresTpl: TemplateRef<any>;
+
+  columns: CdTableColumn[];
+  executingTasks: ExecutingTask[] = [];
+  images: any;
+  modalRef: BsModalRef;
+  retries: number;
+  selection = new CdTableSelection();
+  viewCacheStatusList: any[];
+
+  constructor(
+    private rbdService: RbdService,
+    private modalService: BsModalService,
+    private cdDatePipe: CdDatePipe,
+    private taskListService: TaskListService
+  ) {}
+
+  ngOnInit() {
+    this.columns = [
+      {
+        name: 'ID',
+        prop: 'id',
+        flexGrow: 1,
+        cellTransformation: CellTemplate.executing
+      },
+      {
+        name: 'Name',
+        prop: 'name',
+        flexGrow: 1
+      },
+      {
+        name: 'Pool',
+        prop: 'pool_name',
+        flexGrow: 1
+      },
+      {
+        name: 'Status',
+        prop: 'deferment_end_time',
+        flexGrow: 1,
+        cellTemplate: this.expiresTpl
+      },
+      {
+        name: 'Deleted At',
+        prop: 'deletion_time',
+        flexGrow: 1,
+        pipe: this.cdDatePipe
+      }
+    ];
+
+    this.taskListService.init(
+      () => this.rbdService.listTrash(),
+      (resp) => this.prepareResponse(resp),
+      (images) => (this.images = images),
+      () => this.onFetchError(),
+      this.taskFilter,
+      this.itemFilter,
+      undefined
+    );
+  }
+
+  prepareResponse(resp: any[]): any[] {
+    let images = [];
+    const viewCacheStatusMap = {};
+    resp.forEach((pool) => {
+      if (_.isUndefined(viewCacheStatusMap[pool.status])) {
+        viewCacheStatusMap[pool.status] = [];
+      }
+      viewCacheStatusMap[pool.status].push(pool.pool_name);
+      images = images.concat(pool.value);
+    });
+
+    const viewCacheStatusList = [];
+    _.forEach(viewCacheStatusMap, (value: any, key) => {
+      viewCacheStatusList.push({
+        status: parseInt(key, 10),
+        statusFor:
+          (value.length > 1 ? 'pools ' : 'pool ') +
+          '<strong>' +
+          value.join('</strong>, <strong>') +
+          '</strong>'
+      });
+    });
+    this.viewCacheStatusList = viewCacheStatusList;
+    images.forEach((image) => {
+      image.cdIsExpired = moment().isAfter(image.deferment_end_time);
+    });
+    return images;
+  }
+
+  onFetchError() {
+    this.table.reset(); // Disable loading indicator.
+    this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }];
+  }
+
+  itemFilter(entry, task) {
+    return entry.id === task.metadata['image_id'];
+  }
+
+  taskFilter(task) {
+    return ['rbd/trash/remove', 'rbd/trash/restore'].includes(task.name);
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
+}
index 853fdf9b6b766c96aff1ff4eaa5eb8f96485f346..2eba24eb76c10997e59a8bdf6cfb96813d6334ae 100644 (file)
@@ -96,6 +96,10 @@ export class RbdService {
     });
   }
 
+  listTrash() {
+    return this.http.get(`api/block/image/trash/`);
+  }
+
   moveTrash(poolName, rbdName, delay) {
     return this.http.post(
       `api/block/image/${poolName}/${rbdName}/move_trash`,
index 6b9e355b042f40d761d10f6ae28516045c6a451f..20d5a962e36573ca23bf46b32d2b95feb54b78e0 100644 (file)
@@ -54,7 +54,7 @@ export class TaskListService implements OnDestroy {
     this.onFetchError = onFetchError;
     this.taskFilter = taskFilter;
     this.itemFilter = itemFilter;
-    this.builders = builders;
+    this.builders = builders || {};
 
     this.summaryDataSubscription = this.summaryService.subscribe((tasks: any) => {
       if (tasks) {
@@ -76,7 +76,7 @@ export class TaskListService implements OnDestroy {
   }
 
   private addMissing(data: any[], tasks: ExecutingTask[]) {
-    const defaultBuilder = this.builders['default'];
+    const defaultBuilder = this.builders['default'] || {};
     tasks.forEach((task) => {
       const existing = data.find((item) => this.itemFilter(item, task));
       const builder = this.builders[task.name];