mirror of
https://github.com/ceph/ceph
synced 2025-02-19 08:57:27 +00:00
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:
commit
b2ca51a8bf
@ -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]
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user