From d6eda3b71df77f6f26d3643f1bf1cb5b14a2917c Mon Sep 17 00:00:00 2001 From: Tiago Melo Date: Tue, 19 Jun 2018 15:09:23 +0100 Subject: [PATCH] mgr/dashboard: Add UI for RBD Trash List Signed-off-by: Tiago Melo --- .../frontend/src/app/app-routing.module.ts | 4 +- .../src/app/ceph/block/block.module.ts | 12 +- .../rbd-images/rbd-images.component.html | 13 ++ .../rbd-images/rbd-images.component.scss | 0 .../rbd-images/rbd-images.component.spec.ts | 50 +++++++ .../block/rbd-images/rbd-images.component.ts | 12 ++ .../rbd-trash-list.component.html | 26 ++++ .../rbd-trash-list.component.scss | 0 .../rbd-trash-list.component.spec.ts | 97 +++++++++++++ .../rbd-trash-list.component.ts | 132 ++++++++++++++++++ .../src/app/shared/api/rbd.service.ts | 4 + .../app/shared/services/task-list.service.ts | 4 +- 12 files changed, 344 insertions(+), 10 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 5de3f59300082..efe4ed58db89d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -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' } }, { 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 e6300ee09fec9..8bfd77b00b865 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 @@ -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 index 0000000000000..f629bd7e78da5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.html @@ -0,0 +1,13 @@ +
+ + + + + + + + +
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 index 0000000000000..e69de29bb2d1d 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 index 0000000000000..82cc3f7efbf21 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.spec.ts @@ -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; + + 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 index 0000000000000..78e78ff6b9f39 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.ts @@ -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 index 0000000000000..4933b09848897 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html @@ -0,0 +1,26 @@ + + + + + + + Expired at + + + Protected until + + + {{ value | cdDate }} + 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 index 0000000000000..e69de29bb2d1d 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 index 0000000000000..1bb18d2bdaecb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts @@ -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; + 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 index 0000000000000..8dbfb5f2a81a9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts @@ -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; + + 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 ') + + '' + + value.join(', ') + + '' + }); + }); + 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; + } +} 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 853fdf9b6b766..2eba24eb76c10 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 @@ -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`, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts index 6b9e355b042f4..20d5a962e3657 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts @@ -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]; -- 2.39.5