Skip to content

Commit

Permalink
mgr/dashboard: Add support for managing individual OSD settings/chara…
Browse files Browse the repository at this point in the history
…cteristics in the frontend

Fixes: http://tracker.ceph.com/issues/35448

Signed-off-by: Patrick Nawracay <pnawracay@suse.com>
  • Loading branch information
p-se committed Oct 24, 2018
1 parent c98d502 commit cacb8db
Show file tree
Hide file tree
Showing 29 changed files with 680 additions and 84 deletions.
10 changes: 8 additions & 2 deletions src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
Expand Up @@ -73,9 +73,15 @@ const routes: Routes = [
},
{
path: 'osd',
component: OsdListComponent,
canActivate: [AuthGuardService],
data: { breadcrumbs: 'Cluster/OSDs' }
canActivateChild: [AuthGuardService],
data: { breadcrumbs: 'Cluster/OSDs' },
children: [
{
path: '',
component: OsdListComponent
}
]
},
{
path: 'configuration',
Expand Down
Expand Up @@ -3,9 +3,11 @@ import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { AlertModule } from 'ngx-bootstrap/alert';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { ModalModule } from 'ngx-bootstrap/modal';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { TooltipModule } from 'ngx-bootstrap/tooltip';

import { SharedModule } from '../../shared/shared.module';
import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
Expand All @@ -19,10 +21,16 @@ import { OsdDetailsComponent } from './osd/osd-details/osd-details.component';
import { OsdFlagsModalComponent } from './osd/osd-flags-modal/osd-flags-modal.component';
import { OsdListComponent } from './osd/osd-list/osd-list.component';
import { OsdPerformanceHistogramComponent } from './osd/osd-performance-histogram/osd-performance-histogram.component';
import { OsdReweightModalComponent } from './osd/osd-reweight-modal/osd-reweight-modal.component';
import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.component';

@NgModule({
entryComponents: [OsdDetailsComponent, OsdScrubModalComponent, OsdFlagsModalComponent],
entryComponents: [
OsdDetailsComponent,
OsdScrubModalComponent,
OsdFlagsModalComponent,
OsdReweightModalComponent
],
imports: [
CommonModule,
PerformanceCounterModule,
Expand All @@ -32,7 +40,9 @@ import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.co
FormsModule,
ReactiveFormsModule,
BsDropdownModule.forRoot(),
ModalModule.forRoot()
ModalModule.forRoot(),
AlertModule.forRoot(),
TooltipModule.forRoot()
],
declarations: [
HostsComponent,
Expand All @@ -45,7 +55,8 @@ import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.co
OsdFlagsModalComponent,
HostDetailsComponent,
ConfigurationDetailsComponent,
ConfigurationFormComponent
ConfigurationFormComponent,
OsdReweightModalComponent
]
})
export class ClusterModule {}
Expand Up @@ -136,6 +136,11 @@ <h3 class="panel-title">
i18n>
{{ patternHelpText }}
</span>
<span class="help-block"
*ngIf="configForm.showError(section, formDir, 'invalidUuid')"
i18n>
{{ patternHelpText }}
</span>
<span class="help-block"
*ngIf="configForm.showError(section, formDir, 'max')"
i18n>
Expand Down
Expand Up @@ -7,10 +7,9 @@
selectionType="single"
(updateSelection)="updateSelection($event)"
[updateSelectionOnRefresh]="'never'">
<div class="table-actions btn-toolbar" *ngIf="permission.update">
<div class="table-actions btn-toolbar">
<cd-table-actions [permission]="permission"
[selection]="selection"
onlyDropDown="Perform Task"
class="btn-group"
[tableActions]="tableActions">
</cd-table-actions>
Expand Down Expand Up @@ -53,3 +52,28 @@
</cd-grafana>
</tab>
</tabset>

<ng-template #osdUsageTpl
let-row="row">
<cd-usage-bar [totalBytes]="row.stats.stat_bytes"
[usedBytes]="row.stats.stat_bytes_used"></cd-usage-bar>
</ng-template>

<ng-template #markOsdConfirmationTpl i18n let-markActionDescription="markActionDescription">
<strong>OSD {{ selection.first().id }}</strong> will be marked
<strong>{{ markActionDescription }}</strong> if you proceed.
</ng-template>

<ng-template #criticalConfirmationTpl
i18n
let-safeToDestroyResult="result"
let-actionDescription="actionDescription">
<div *ngIf="!safeToDestroyResult['safe-to-destroy']"
class="danger">
<cd-warning-panel>
{{ safeToDestroyResult.message }}
</cd-warning-panel>
</div>
<strong>OSD {{ selection.first().id }}</strong> will be
<strong>{{ actionDescription }}</strong> if you proceed.
</ng-template>
@@ -1,23 +1,34 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';

import { BsModalService } from 'ngx-bootstrap/modal';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { EMPTY, of } from 'rxjs';
import Spy = jasmine.Spy;

import { configureTestBed, PermissionHelper } from '../../../../../testing/unit-test-helper';
import { OsdService } from '../../../../shared/api/osd.service';
import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component';
import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
import { Permissions } from '../../../../shared/models/permissions';
import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
import { SharedModule } from '../../../../shared/shared.module';
import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
import { OsdDetailsComponent } from '../osd-details/osd-details.component';
import { OsdPerformanceHistogramComponent } from '../osd-performance-histogram/osd-performance-histogram.component';
import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
import { OsdListComponent } from './osd-list.component';

describe('OsdListComponent', () => {
let component: OsdListComponent;
let fixture: ComponentFixture<OsdListComponent>;
let modalServiceShowSpy: Spy;

const fakeAuthStorageService = {
getPermissions: () => {
Expand All @@ -31,17 +42,37 @@ describe('OsdListComponent', () => {
PerformanceCounterModule,
TabsModule.forRoot(),
SharedModule,
ReactiveFormsModule,
RouterTestingModule
],
declarations: [OsdListComponent, OsdDetailsComponent, OsdPerformanceHistogramComponent],
providers: [{ provide: AuthStorageService, useValue: fakeAuthStorageService }]
providers: [
{ provide: AuthStorageService, useValue: fakeAuthStorageService },
TableActionsComponent,
OsdService,
BsModalService
]
});

beforeEach(() => {
fixture = TestBed.createComponent(OsdListComponent);
fixture.detectChanges();
component = fixture.componentInstance;
modalServiceShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.stub();
});

const setFakeSelection = () => {
// Default data and selection
const selection = [{ id: 1 }];
const data = [{ id: 1 }];

// Table data and selection
component.selection = new CdTableSelection();
component.selection.selected = selection;
component.selection.update();
component.osds = data;
};

it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
Expand All @@ -62,25 +93,123 @@ describe('OsdListComponent', () => {
getTableActionComponent()
);
scenario = {
fn: () => tableActions.getCurrentButton(),
single: undefined,
empty: undefined
fn: () => tableActions.getCurrentButton().name,
single: 'Scrub',
empty: 'Scrub'
};
tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 1);
});

it('shows no action button', () => permissionHelper.testScenarios(scenario));
it('shows action button', () => permissionHelper.testScenarios(scenario));

it('shows all actions', () => {
expect(tableActions.tableActions.length).toBe(2);
expect(tableActions.tableActions.length).toBe(9);
expect(tableActions.tableActions).toEqual(component.tableActions);
});
});

describe('test table actions in submenu', () => {
beforeEach(
fakeAsync(() => {
// The menu needs a click to render the dropdown!
const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
dropDownToggle.triggerEventHandler('click', null);
tick();
fixture.detectChanges();
})
);

/**
* Helper function to retrieve menu item
* @param selector
*/
const getMenuItem = (selector: string): DebugElement => {
return fixture.debugElement
.query(By.directive(TableActionsComponent))
.query(By.css(selector));
};

it('has menu entries disabled for entries without create permission', () => {
component.tableActions
.filter((tableAction) => tableAction.permission !== 'create')
.map((tableAction) => tableAction.name)
.map(TestBed.get(TableActionsComponent).toClassName)
.map((className) => getMenuItem(`.${className}`))
.forEach((debugElement) => {
expect(debugElement.classes.disabled).toBe(true);
});
});
});

describe('tests if all modals are opened correctly', () => {
/**
* Helper function to check if a function opens a modal
* @param fn
* @param modalClass - The expected class of the modal
*/
const expectOpensModal = (fn, modalClass): void => {
setFakeSelection();
fn();

expect(modalServiceShowSpy.calls.any()).toBe(true, 'modalService.show called');
expect(modalServiceShowSpy.calls.first()).toBeTruthy();
expect(modalServiceShowSpy.calls.first().args[0]).toBe(modalClass);

modalServiceShowSpy.calls.reset();
};

it('opens the appropriate modal', () => {
expectOpensModal(() => component.reweight(), OsdReweightModalComponent);
expectOpensModal(() => component.markOut(), ConfirmationModalComponent);
expectOpensModal(() => component.markIn(), ConfirmationModalComponent);
expectOpensModal(() => component.markDown(), ConfirmationModalComponent);

// The following modals are called after the information about their
// safety to destroy/remove/mark them lost has been retrieved, hence
// we will have to fake its request to be able to open those modals.
spyOn(TestBed.get(OsdService), 'safeToDestroy').and.callFake(() =>
of({ 'safe-to-destroy': true })
);

expectOpensModal(() => component.markLost(), CriticalConfirmationModalComponent);
expectOpensModal(() => component.remove(), CriticalConfirmationModalComponent);
expectOpensModal(() => component.destroy(), CriticalConfirmationModalComponent);
});
});

describe('tests if the correct methods are called on confirmation', () => {
const expectOsdServiceMethodCalled = (fn: Function, osdServiceMethodName: string): void => {
setFakeSelection();
const osdServiceSpy = spyOn(TestBed.get(OsdService), osdServiceMethodName).and.callFake(
() => EMPTY
);

modalServiceShowSpy.calls.reset();
fn(); // calls show on BsModalService
// Calls onSubmit given to `bsModalService.show()`
const initialState = modalServiceShowSpy.calls.first().args[1].initialState;
const action = initialState.onSubmit || initialState.submitAction;
action.call(component);

expect(osdServiceSpy.calls.count()).toBe(1);
expect(osdServiceSpy.calls.first().args[0]).toBe(1);
modalServiceShowSpy.calls.reset();
osdServiceSpy.calls.reset();
};

it('calls the corresponding service methods', () => {
// Purposely `reweight`
expectOsdServiceMethodCalled(() => component.markOut(), 'markOut');
expectOsdServiceMethodCalled(() => component.markIn(), 'markIn');
expectOsdServiceMethodCalled(() => component.markDown(), 'markDown');

spyOn(TestBed.get(OsdService), 'safeToDestroy').and.callFake(() =>
of({ 'safe-to-destroy': true })
);

it(`shows 'Perform task' as drop down`, () => {
expect(
fixture.debugElement.query(By.directive(TableActionsComponent)).query(By.css('button'))
.nativeElement.textContent
).toBe('Perform Task');
expectOsdServiceMethodCalled(() => component.markLost(), 'markLost');
expectOsdServiceMethodCalled(() => component.remove(), 'remove');
expectOsdServiceMethodCalled(() => component.destroy(), 'destroy');
});
});
});

0 comments on commit cacb8db

Please sign in to comment.