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';
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' } },
{
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,
RbdFormComponent,
RbdSnapshotListComponent,
RbdSnapshotFormComponent,
- RbdTrashMoveModalComponent
+ RbdTrashListComponent,
+ RbdTrashMoveModalComponent,
+ RbdImagesComponent
]
})
export class BlockModule {}
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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() {}
+}
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+ });
+});
--- /dev/null
+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;
+ }
+}
});
}
+ listTrash() {
+ return this.http.get(`api/block/image/trash/`);
+ }
+
moveTrash(poolName, rbdName, delay) {
return this.http.post(
`api/block/image/${poolName}/${rbdName}/move_trash`,
this.onFetchError = onFetchError;
this.taskFilter = taskFilter;
this.itemFilter = itemFilter;
- this.builders = builders;
+ this.builders = builders || {};
this.summaryDataSubscription = this.summaryService.subscribe((tasks: any) => {
if (tasks) {
}
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];