mirror of
https://github.com/ceph/ceph
synced 2025-03-23 18:58:41 +00:00
mgr/dashboard: Add UI for RBD Trash Purge
Signed-off-by: Tiago Melo <tmelo@suse.com>
This commit is contained in:
parent
11067e0686
commit
1eeaa5016b
src/pybind/mgr/dashboard/frontend/src/app
@ -19,6 +19,7 @@ import { RbdSnapshotFormComponent } from './rbd-snapshot-form/rbd-snapshot-form.
|
|||||||
import { RbdSnapshotListComponent } from './rbd-snapshot-list/rbd-snapshot-list.component';
|
import { RbdSnapshotListComponent } from './rbd-snapshot-list/rbd-snapshot-list.component';
|
||||||
import { RbdTrashListComponent } from './rbd-trash-list/rbd-trash-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 { 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';
|
import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-trash-restore-modal.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -26,7 +27,8 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra
|
|||||||
RbdDetailsComponent,
|
RbdDetailsComponent,
|
||||||
RbdSnapshotFormComponent,
|
RbdSnapshotFormComponent,
|
||||||
RbdTrashMoveModalComponent,
|
RbdTrashMoveModalComponent,
|
||||||
RbdTrashRestoreModalComponent
|
RbdTrashRestoreModalComponent,
|
||||||
|
RbdTrashPurgeModalComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -53,7 +55,8 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra
|
|||||||
RbdTrashListComponent,
|
RbdTrashListComponent,
|
||||||
RbdTrashMoveModalComponent,
|
RbdTrashMoveModalComponent,
|
||||||
RbdImagesComponent,
|
RbdImagesComponent,
|
||||||
RbdTrashRestoreModalComponent
|
RbdTrashRestoreModalComponent,
|
||||||
|
RbdTrashPurgeModalComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class BlockModule {}
|
export class BlockModule {}
|
||||||
|
@ -15,6 +15,14 @@
|
|||||||
[selection]="selection"
|
[selection]="selection"
|
||||||
[tableActions]="tableActions">
|
[tableActions]="tableActions">
|
||||||
</cd-table-actions>
|
</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>
|
</div>
|
||||||
</cd-table>
|
</cd-table>
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
|
|||||||
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
|
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
|
||||||
import { TaskListService } from '../../../shared/services/task-list.service';
|
import { TaskListService } from '../../../shared/services/task-list.service';
|
||||||
import { TaskWrapperService } from '../../../shared/services/task-wrapper.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';
|
import { RbdTrashRestoreModalComponent } from '../rbd-trash-restore-modal/rbd-trash-restore-modal.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -197,4 +198,8 @@ export class RbdTrashListComponent implements OnInit {
|
|||||||
isExpired(expiresAt): boolean {
|
isExpired(expiresAt): boolean {
|
||||||
return moment().isAfter(expiresAt);
|
return moment().isAfter(expiresAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
purgeModal() {
|
||||||
|
this.modalService.show(RbdTrashPurgeModalComponent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
<kbd i18n>Purge Trash</kbd>.
|
||||||
|
</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>
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
restoreTrash(poolName, imageId, newImageName) {
|
||||||
return this.http.post(
|
return this.http.post(
|
||||||
`api/block/image/trash/${poolName}/${imageId}/restore`,
|
`api/block/image/trash/${poolName}/${imageId}/restore`,
|
||||||
|
Loading…
Reference in New Issue
Block a user