mgr/dashboard Adds multiple selection in osd table (#29662)

mgr/dashboard Adds multiple selection in osd table

Reviewed-by: Patrick Seidensal <pnawracay@suse.com>
Reviewed-by: Stephan Müller <smueller@suse.com>
Reviewed-by: Tiago Melo <tmelo@suse.com>
Reviewed-by: Volker Theile <vtheile@suse.com>
This commit is contained in:
Lenz Grimmer 2019-09-20 09:21:28 +00:00 committed by GitHub
commit b2ca51a8bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 84 additions and 66 deletions

View File

@ -6,11 +6,12 @@ import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { ChartsModule } from 'ng2-charts';
import { AlertModule } from 'ngx-bootstrap/alert';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { PopoverModule } from 'ngx-bootstrap/popover';
import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
import { ErrorPanelComponent } from '../../../shared/components/error-panel/error-panel.component';
import { SparklineComponent } from '../../../shared/components/sparkline/sparkline.component';
import { TableComponent } from '../../../shared/datatable/table/table.component';
import { ComponentsModule } from '../../../shared/components/components.module';
import { RbdConfigurationEntry } from '../../../shared/models/configuration';
import { PipesModule } from '../../../shared/pipes/pipes.module';
import { FormatterService } from '../../../shared/services/formatter.service';
@ -26,17 +27,14 @@ describe('RbdConfigurationListComponent', () => {
FormsModule,
NgxDatatableModule,
RouterTestingModule,
ComponentsModule,
AlertModule,
BsDropdownModule.forRoot(),
ChartsModule,
PipesModule
],
declarations: [
RbdConfigurationListComponent,
TableComponent,
ErrorPanelComponent,
SparklineComponent
PipesModule,
PopoverModule
],
declarations: [RbdConfigurationListComponent, TableComponent],
providers: [FormatterService, RbdConfigurationService, i18nProviders]
});

View File

@ -1,12 +1,14 @@
<tabset>
<tab i18n-heading
heading="OSDs List">
<cd-table [data]="osds"
(fetchData)="getOsdList()"
[columns]="columns"
selectionType="single"
selectionType="multi"
(updateSelection)="updateSelection($event)"
[updateSelectionOnRefresh]="'never'">
<div class="table-actions btn-toolbar">
<cd-table-actions [permission]="permissions.osd"
[selection]="selection"
@ -57,7 +59,7 @@
<ng-template #markOsdConfirmationTpl
let-markActionDescription="markActionDescription">
<ng-container i18n><strong>OSD {{ selection.first().id }}</strong> will be marked
<ng-container i18n><strong>OSD(s) {{ getSelectedIds() | list }}</strong> will be marked
<strong>{{ markActionDescription }}</strong> if you proceed.</ng-container>
</ng-template>
@ -66,8 +68,8 @@
let-actionDescription="actionDescription">
<div *ngIf="!safeToDestroyResult['is_safe_to_destroy']"
class="danger">
<cd-warning-panel i18n>The OSD is not safe to destroy!</cd-warning-panel>
<cd-warning-panel i18n>The {selection.hasSingleSelection, select, 1 {OSD is} 0 {OSDs are}} not safe to destroy!</cd-warning-panel>
</div>
<ng-container i18n><strong>OSD {{ selection.first().id }}</strong> will be
<ng-container i18n><strong>OSD {{ getSelectedIds() | list }}</strong> will be
<strong>{{ actionDescription }}</strong> if you proceed.</ng-container>
</ng-template>

View File

@ -1,8 +1,9 @@
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { I18n } from '@ngx-translate/i18n-polyfill';
import * as _ from 'lodash';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { Observable } from 'rxjs';
import { forkJoin as observableForkJoin, Observable } from 'rxjs';
import { OsdService } from '../../../../shared/api/osd.service';
import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
@ -51,8 +52,8 @@ export class OsdListComponent implements OnInit {
clusterWideActions: CdTableAction[];
icons = Icons;
osds = [];
selection = new CdTableSelection();
osds = [];
protected static collectStates(osd) {
return [osd['in'] ? 'in' : 'out', osd['up'] ? 'up' : 'down'];
@ -86,7 +87,7 @@ export class OsdListComponent implements OnInit {
name: this.actionLabels.REWEIGHT,
permission: 'update',
click: () => this.reweight(),
disable: () => !this.hasOsdSelected,
disable: () => !this.hasOsdSelected || !this.selection.hasSingleSelection,
icon: Icons.reweight
},
{
@ -206,11 +207,17 @@ export class OsdListComponent implements OnInit {
];
}
getSelectedIds() {
return this.selection.selected.map((row) => row.id);
}
get hasOsdSelected() {
const validOsds = [];
if (this.selection.hasSelection) {
const osdId = this.selection.first().id;
const osd = this.osds.filter((o) => o.id === osdId).pop();
return !!osd;
for (const osdId of this.getSelectedIds()) {
validOsds.push(this.osds.filter((o) => o.id === osdId).pop());
}
return validOsds.length > 0;
}
return false;
}
@ -228,23 +235,27 @@ export class OsdListComponent implements OnInit {
return true;
}
const osdId = this.selection.first().id;
const osd = this.osds.filter((o) => o.id === osdId).pop();
const validOsds = [];
if (this.selection.hasSelection) {
for (const osdId of this.getSelectedIds()) {
validOsds.push(this.osds.filter((o) => o.id === osdId).pop());
}
}
if (!osd) {
if (validOsds.length === 0) {
// `osd` is undefined if the selected OSD has been removed.
return true;
}
switch (state) {
case 'in':
return osd.in === 1;
return validOsds.some((osd) => osd.in === 1);
case 'out':
return osd.in !== 1;
return validOsds.some((osd) => osd.in !== 1);
case 'down':
return osd.up !== 1;
return validOsds.some((osd) => osd.up !== 1);
case 'up':
return osd.up === 1;
return validOsds.some((osd) => osd.up === 1);
}
}
@ -267,7 +278,7 @@ export class OsdListComponent implements OnInit {
}
const initialState = {
selected: this.tableComponent.selection.selected,
selected: this.getSelectedIds(),
deep: deep
};
@ -288,9 +299,11 @@ export class OsdListComponent implements OnInit {
markActionDescription: markAction
},
onSubmit: () => {
onSubmit
.call(this.osdService, this.selection.first().id)
.subscribe(() => this.bsModalRef.hide());
observableForkJoin(
this.getSelectedIds().map((osd: any) => {
onSubmit.call(this.osdService, osd).subscribe(() => this.bsModalRef.hide());
})
);
}
}
});
@ -312,7 +325,7 @@ export class OsdListComponent implements OnInit {
templateItemDescription: string,
action: (id: number) => Observable<any>
): void {
this.osdService.safeToDestroy(this.selection.first().id).subscribe((result) => {
this.osdService.safeToDestroy(JSON.stringify(this.getSelectedIds())).subscribe((result) => {
const modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
initialState: {
actionDescription: actionDescription,
@ -323,9 +336,11 @@ export class OsdListComponent implements OnInit {
actionDescription: templateItemDescription
},
submitAction: () => {
action
.call(this.osdService, this.selection.first().id)
.subscribe(() => modalRef.hide());
observableForkJoin(
this.getSelectedIds().map((osd: any) => {
action.call(this.osdService, osd).subscribe(() => modalRef.hide());
})
);
}
}
});

View File

@ -1,6 +1,6 @@
<cd-modal [modalRef]="bsModalRef">
<ng-container class="modal-title"
i18n>Reweight OSD</ng-container>
i18n>Reweight OSD: {{ osdId }}</ng-container>
<ng-container class="modal-content">
<form [formGroup]="reweightForm">

View File

@ -8,10 +8,8 @@
[formGroup]="scrubForm"
novalidate>
<div class="modal-body">
<div *ngIf="selected.length === 1">
<p i18n>You are about to apply a {deep, select, 1 {deep }}scrub to
the OSD <strong>{{ selected[0].id }}</strong>.</p>
</div>
<p i18n>You are about to apply a {deep, select, 1 {deep }}scrub to
the OSD(s): <strong>{{ selected | list }}</strong>.</p>
</div>
<div class="modal-footer">

View File

@ -3,9 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { BsModalRef } from 'ngx-bootstrap/modal';
import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
import { OsdService } from '../../../../shared/api/osd.service';
import { ListPipe } from '../../../../shared/pipes/list.pipe';
import { NotificationService } from '../../../../shared/services/notification.service';
import { OsdScrubModalComponent } from './osd-scrub-modal.component';
@ -27,10 +27,11 @@ describe('OsdScrubModalComponent', () => {
configureTestBed({
imports: [ReactiveFormsModule],
declarations: [OsdScrubModalComponent],
declarations: [OsdScrubModalComponent, ListPipe],
schemas: [NO_ERRORS_SCHEMA],
providers: [
BsModalRef,
ListPipe,
{ provide: OsdService, useValue: fakeService },
{ provide: NotificationService, useValue: fakeService },
i18nProviders

View File

@ -6,6 +6,7 @@ import { BsModalRef } from 'ngx-bootstrap/modal';
import { OsdService } from '../../../../shared/api/osd.service';
import { NotificationType } from '../../../../shared/enum/notification-type.enum';
import { ListPipe } from '../../../../shared/pipes/list.pipe';
import { NotificationService } from '../../../../shared/services/notification.service';
@Component({
@ -15,14 +16,15 @@ import { NotificationService } from '../../../../shared/services/notification.se
})
export class OsdScrubModalComponent implements OnInit {
deep: boolean;
selected = [];
scrubForm: FormGroup;
selected = [];
constructor(
public bsModalRef: BsModalRef,
private osdService: OsdService,
private notificationService: NotificationService,
private i18n: I18n
private i18n: I18n,
private listPipe: ListPipe
) {}
ngOnInit() {
@ -30,25 +32,24 @@ export class OsdScrubModalComponent implements OnInit {
}
scrub() {
const id = this.selected[0].id;
for (const id of this.selected) {
this.osdService.scrub(id, this.deep).subscribe(
() => {
const operation = this.deep ? 'Deep scrub' : 'Scrub';
this.osdService.scrub(id, this.deep).subscribe(
() => {
const operation = this.deep ? 'Deep scrub' : 'Scrub';
this.notificationService.show(
NotificationType.success,
this.i18n('{{operation}} was initialized in the following OSD: {{id}}', {
operation: operation,
id: id
})
);
this.bsModalRef.hide();
},
() => {
this.bsModalRef.hide();
}
);
this.notificationService.show(
NotificationType.success,
this.i18n('{{operation}} was initialized in the following OSD(s): {{id}}', {
operation: operation,
id: this.listPipe.transform(this.selected)
})
);
this.bsModalRef.hide();
},
() => {
this.bsModalRef.hide();
}
);
}
}
}

View File

@ -107,8 +107,8 @@ describe('OsdService', () => {
});
it('should return if it is safe to destroy an OSD', () => {
service.safeToDestroy(1).subscribe();
const req = httpTesting.expectOne('api/osd/1/safe_to_destroy');
service.safeToDestroy('[0,1]').subscribe();
const req = httpTesting.expectOne('api/osd/[0,1]/safe_to_destroy');
expect(req.request.method).toBe('GET');
});
});

View File

@ -215,11 +215,11 @@ export class OsdService {
return this.http.post(`${this.path}/${id}/destroy`, null);
}
safeToDestroy(id: number) {
safeToDestroy(ids: string) {
interface SafeToDestroyResponse {
'safe-to-destroy': boolean;
message?: string;
}
return this.http.get<SafeToDestroyResponse>(`${this.path}/${id}/safe_to_destroy`);
return this.http.get<SafeToDestroyResponse>(`${this.path}/${ids}/safe_to_destroy`);
}
}

View File

@ -118,6 +118,9 @@
let-offset="offset"
let-isVisible="isVisible">
<div class="page-count">
<span *ngIf="selectionType == 'multi'">
<cd-helper i18n>Press and hold control button to select multiple rows to execute a common action on.</cd-helper>
</span>
<span *ngIf="selectionType">
{{ selectedCount }} <ng-container i18n="X selected">selected</ng-container> /
</span>