mgr/dashboard: Add UI for RBD Trash Purge

Signed-off-by: Tiago Melo <tmelo@suse.com>
This commit is contained in:
Tiago Melo 2018-07-03 10:59:03 +01:00 committed by Tiago Melo
parent 11067e0686
commit 1eeaa5016b
8 changed files with 254 additions and 2 deletions

View File

@ -19,6 +19,7 @@ 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 { RbdTrashPurgeModalComponent } from './rbd-trash-purge-modal/rbd-trash-purge-modal.component';
import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-trash-restore-modal.component';
@NgModule({
@ -26,7 +27,8 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra
RbdDetailsComponent,
RbdSnapshotFormComponent,
RbdTrashMoveModalComponent,
RbdTrashRestoreModalComponent
RbdTrashRestoreModalComponent,
RbdTrashPurgeModalComponent
],
imports: [
CommonModule,
@ -53,7 +55,8 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra
RbdTrashListComponent,
RbdTrashMoveModalComponent,
RbdImagesComponent,
RbdTrashRestoreModalComponent
RbdTrashRestoreModalComponent,
RbdTrashPurgeModalComponent
]
})
export class BlockModule {}

View File

@ -15,6 +15,14 @@
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
<button class="btn btn-sm btn-default btn-label"
type="button"
(click)="purgeModal()">
<i class="fa fa-fw fa-times"
aria-hidden="true"></i>
<ng-container i18n>Purge Trash</ng-container>
</button>
</div>
</cd-table>

View File

@ -19,6 +19,7 @@ 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 { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
import { RbdTrashPurgeModalComponent } from '../rbd-trash-purge-modal/rbd-trash-purge-modal.component';
import { RbdTrashRestoreModalComponent } from '../rbd-trash-restore-modal/rbd-trash-restore-modal.component';
@Component({
@ -197,4 +198,8 @@ export class RbdTrashListComponent implements OnInit {
isExpired(expiresAt): boolean {
return moment().isAfter(expiresAt);
}
purgeModal() {
this.modalService.show(RbdTrashPurgeModalComponent);
}
}

View File

@ -0,0 +1,53 @@
<cd-modal>
<ng-container i18n
class="modal-title">Purge Trash</ng-container>
<ng-container class="modal-content">
<form name="purgeForm"
class="form"
#formDir="ngForm"
[formGroup]="purgeForm"
novalidate>
<div class="modal-body">
<p>
<ng-container i18n>To purge, select one or All images and click</ng-container>&nbsp;
<kbd i18n>Purge Trash</kbd>.&nbsp;
</p>
<div class="form-group">
<label class="center-block"
i18n>Pool:
</label>
<input class="form-control"
type="text"
placeholder="Pool name..."
i18n-placeholder
formControlName="poolName"
*ngIf="!poolPermission.read">
<select class="form-control"
formControlName="poolName"
*ngIf="poolPermission.read">
<option value=""
i18n>All</option>
<option *ngFor="let pool of pools"
[value]="pool">{{ pool }}</option>
</select>
</div>
</div>
<div class="modal-footer">
<div class="button-group text-right">
<cd-submit-button i18n
[form]="purgeForm"
(submitAction)="purge()">
Purge Trash
</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>

View File

@ -0,0 +1,105 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ComponentFixture, fakeAsync, TestBed, tick } 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 { Permission } from '../../../shared/models/permissions';
import { NotificationService } from '../../../shared/services/notification.service';
import { SharedModule } from '../../../shared/shared.module';
import { RbdTrashPurgeModalComponent } from './rbd-trash-purge-modal.component';
describe('RbdTrashPurgeModalComponent', () => {
let component: RbdTrashPurgeModalComponent;
let fixture: ComponentFixture<RbdTrashPurgeModalComponent>;
let httpTesting: HttpTestingController;
configureTestBed({
imports: [
HttpClientTestingModule,
ReactiveFormsModule,
SharedModule,
ToastModule.forRoot(),
RouterTestingModule
],
declarations: [RbdTrashPurgeModalComponent],
providers: [BsModalRef]
});
beforeEach(() => {
fixture = TestBed.createComponent(RbdTrashPurgeModalComponent);
httpTesting = TestBed.get(HttpTestingController);
component = fixture.componentInstance;
});
it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it(
'should finish ngOnInit',
fakeAsync(() => {
component.poolPermission = new Permission(['read', 'create', 'update', 'delete']);
fixture.detectChanges();
const req = httpTesting.expectOne('api/pool?attrs=pool_name,application_metadata');
req.flush([
{
application_metadata: ['foo'],
pool_name: 'bar'
},
{
application_metadata: ['rbd'],
pool_name: 'baz'
}
]);
tick();
expect(component.pools).toEqual(['baz']);
expect(component.purgeForm).toBeTruthy();
})
);
it('should call ngOnInit without pool permissions', () => {
component.poolPermission = new Permission([]);
component.ngOnInit();
httpTesting.expectOne('api/summary');
httpTesting.verify();
});
describe('should call purge', () => {
let notificationService: NotificationService;
let modalRef: BsModalRef;
let req;
beforeEach(() => {
fixture.detectChanges();
notificationService = TestBed.get(NotificationService);
modalRef = TestBed.get(BsModalRef);
component.purgeForm.patchValue({ poolName: 'foo' });
spyOn(modalRef, 'hide').and.stub();
spyOn(component.purgeForm, 'setErrors').and.stub();
spyOn(notificationService, 'show').and.stub();
component.purge();
req = httpTesting.expectOne('api/block/image/trash/purge/?pool_name=foo');
});
it('with success', () => {
req.flush(null);
expect(component.purgeForm.setErrors).toHaveBeenCalledTimes(0);
expect(component.modalRef.hide).toHaveBeenCalledTimes(1);
});
it('with failure', () => {
req.flush(null, { status: 500, statusText: 'failure' });
expect(component.purgeForm.setErrors).toHaveBeenCalledTimes(1);
expect(component.modalRef.hide).toHaveBeenCalledTimes(0);
});
});
});

View File

@ -0,0 +1,72 @@
import { Component, OnInit } from '@angular/core';
import { BsModalRef } from 'ngx-bootstrap';
import { PoolService } from '../../../shared/api/pool.service';
import { RbdService } from '../../../shared/api/rbd.service';
import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
import { CdFormGroup } from '../../../shared/forms/cd-form-group';
import { FinishedTask } from '../../../shared/models/finished-task';
import { Permission } from '../../../shared/models/permissions';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
@Component({
selector: 'cd-rbd-trash-purge-modal',
templateUrl: './rbd-trash-purge-modal.component.html',
styleUrls: ['./rbd-trash-purge-modal.component.scss']
})
export class RbdTrashPurgeModalComponent implements OnInit {
poolPermission: Permission;
purgeForm: CdFormGroup;
pools: any[];
constructor(
private authStorageService: AuthStorageService,
private rbdService: RbdService,
public modalRef: BsModalRef,
private fb: CdFormBuilder,
private poolService: PoolService,
private taskWrapper: TaskWrapperService
) {
this.poolPermission = this.authStorageService.getPermissions().pool;
}
createForm() {
this.purgeForm = this.fb.group({
poolName: ''
});
}
ngOnInit() {
if (this.poolPermission.read) {
this.poolService.list(['pool_name', 'application_metadata']).then((resp) => {
this.pools = resp
.filter((pool) => pool.application_metadata.includes('rbd'))
.map((pool) => pool.pool_name);
});
}
this.createForm();
}
purge() {
const poolName = this.purgeForm.getValue('poolName') || '';
this.taskWrapper
.wrapTaskAroundCall({
task: new FinishedTask('rbd/trash/purge', {
pool_name: poolName
}),
call: this.rbdService.purgeTrash(poolName)
})
.subscribe(
undefined,
() => {
this.purgeForm.setErrors({ cdSubmitButton: true });
},
() => {
this.modalRef.hide();
}
);
}
}

View File

@ -108,6 +108,12 @@ export class RbdService {
);
}
purgeTrash(poolName) {
return this.http.post(`api/block/image/trash/purge/?pool_name=${poolName}`, null, {
observe: 'response'
});
}
restoreTrash(poolName, imageId, newImageName) {
return this.http.post(
`api/block/image/trash/${poolName}/${imageId}/restore`,