Skip to content

Commit

Permalink
nautilus: mgr/dashboard: Make all columns sortable (#27889)
Browse files Browse the repository at this point in the history
nautilus: mgr/dashboard: Make all columns sortable

Reviewed-by: Alfonso Martínez <almartin@redhat.com>
  • Loading branch information
Lenz Grimmer committed May 7, 2019
2 parents d6d9eaa + 55d4a0c commit e71c2e6
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 24 deletions.
Expand Up @@ -4,6 +4,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';

import _ = require('lodash');
import { BsModalService } from 'ngx-bootstrap/modal';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { EMPTY, of } from 'rxjs';
Expand Down Expand Up @@ -32,6 +33,7 @@ describe('OsdListComponent', () => {
let component: OsdListComponent;
let fixture: ComponentFixture<OsdListComponent>;
let modalServiceShowSpy: jasmine.Spy;
let osdService: OsdService;

const fakeAuthStorageService = {
getPermissions: () => {
Expand Down Expand Up @@ -92,6 +94,7 @@ describe('OsdListComponent', () => {
fixture = TestBed.createComponent(OsdListComponent);
fixture.detectChanges();
component = fixture.componentInstance;
osdService = TestBed.get(OsdService);
modalServiceShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.stub();
});

Expand All @@ -100,6 +103,73 @@ describe('OsdListComponent', () => {
expect(component).toBeTruthy();
});

it('should have columns that are sortable', () => {
expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
});

describe('getOsdList', () => {
let osds;

const createOsd = (n: number) => ({
in: 'in',
up: 'up',
stats_history: {
op_out_bytes: [[n, n], [n * 2, n * 2]],
op_in_bytes: [[n * 3, n * 3], [n * 4, n * 4]]
},
stats: {
stat_bytes_used: n * n,
stat_bytes: n * n * n
}
});

const expectAttributeOnEveryOsd = (attr: string) =>
expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();

beforeEach(() => {
spyOn(osdService, 'getList').and.callFake(() => of(osds));
osds = [createOsd(1), createOsd(2), createOsd(3)];
component.getOsdList();
});

it('should replace "this.osds" with new data', () => {
expect(component.osds.length).toBe(3);
expect(osdService.getList).toHaveBeenCalledTimes(1);

osds = [createOsd(4)];
component.getOsdList();
expect(component.osds.length).toBe(1);
expect(osdService.getList).toHaveBeenCalledTimes(2);
});

it('should have custom attribute "collectedStates"', () => {
expectAttributeOnEveryOsd('collectedStates');
expect(component.osds[0].collectedStates).toEqual(['in', 'up']);
});

it('should have custom attribute "stats_history.out_bytes"', () => {
expectAttributeOnEveryOsd('stats_history.out_bytes');
expect(component.osds[0].stats_history.out_bytes).toEqual([1, 2]);
});

it('should have custom attribute "stats_history.in_bytes"', () => {
expectAttributeOnEveryOsd('stats_history.in_bytes');
expect(component.osds[0].stats_history.in_bytes).toEqual([3, 4]);
});

it('should have custom attribute "stats.usage"', () => {
expectAttributeOnEveryOsd('stats.usage');
expect(component.osds[0].stats.usage).toBe(1);
expect(component.osds[1].stats.usage).toBe(0.5);
expect(component.osds[2].stats.usage).toBe(3 / 9);
});

it('should have custom attribute "cdIsBinary" to be true', () => {
expectAttributeOnEveryOsd('cdIsBinary');
expect(component.osds[0].cdIsBinary).toBe(true);
});
});

describe('show table actions as defined', () => {
let tableActions: TableActionsComponent;
let scenario: { fn; empty; single };
Expand Down Expand Up @@ -190,11 +260,9 @@ describe('OsdListComponent', () => {
describe('tests if the correct methods are called on confirmation', () => {
const expectOsdServiceMethodCalled = (
actionName: string,
osdServiceMethodName: string
osdServiceMethodName: 'markOut' | 'markIn' | 'markDown' | 'markLost' | 'purge' | 'destroy'
): void => {
const osdServiceSpy = spyOn(TestBed.get(OsdService), osdServiceMethodName).and.callFake(
() => EMPTY
);
const osdServiceSpy = spyOn(osdService, osdServiceMethodName).and.callFake(() => EMPTY);
openActionModal(actionName);
const initialState = modalServiceShowSpy.calls.first().args[1].initialState;
const submit = initialState.onSubmit || initialState.submitAction;
Expand Down
Expand Up @@ -153,7 +153,7 @@ export class OsdListComponent implements OnInit {
{ prop: 'collectedStates', name: this.i18n('Status'), cellTemplate: this.statusColor },
{ prop: 'stats.numpg', name: this.i18n('PGs') },
{ prop: 'stats.stat_bytes', name: this.i18n('Size'), pipe: this.dimlessBinaryPipe },
{ name: this.i18n('Usage'), cellTemplate: this.osdUsageTpl },
{ prop: 'stats.usage', name: this.i18n('Usage'), cellTemplate: this.osdUsageTpl },
{
prop: 'stats_history.out_bytes',
name: this.i18n('Read bytes'),
Expand Down Expand Up @@ -221,11 +221,11 @@ export class OsdListComponent implements OnInit {

getOsdList() {
this.osdService.getList().subscribe((data: any[]) => {
this.osds = data;
data.map((osd) => {
this.osds = data.map((osd) => {
osd.collectedStates = OsdListComponent.collectStates(osd);
osd.stats_history.out_bytes = osd.stats_history.op_out_bytes.map((i) => i[1]);
osd.stats_history.in_bytes = osd.stats_history.op_in_bytes.map((i) => i[1]);
osd.stats.usage = osd.stats.stat_bytes_used / osd.stats.stat_bytes;
osd.cdIsBinary = true;
return osd;
});
Expand Down
Expand Up @@ -28,6 +28,10 @@ describe('TablePerformanceCounterComponent', () => {
expect(component.counters).toEqual([]);
});

it('should have columns that are sortable', () => {
expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
});

describe('Error handling', () => {
const context = new CdTableFetchDataContext(() => {});

Expand Down
Expand Up @@ -49,6 +49,7 @@ export class TablePerformanceCounterComponent implements OnInit {
},
{
name: this.i18n('Value'),
prop: 'value',
cellTemplate: this.valueTpl,
flexGrow: 1
}
Expand Down
Expand Up @@ -63,6 +63,10 @@ describe('PoolListComponent', () => {
expect(component).toBeTruthy();
});

it('should have columns that are sortable', () => {
expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
});

describe('pool deletion', () => {
let taskWrapper: TaskWrapperService;

Expand Down Expand Up @@ -206,7 +210,11 @@ describe('PoolListComponent', () => {
it('transforms pools data correctly', () => {
const pools = [
{
stats: { rd_bytes: { latest: 6, rate: 4, series: [[0, 2], [1, 6]] } },
stats: {
bytes_used: { latest: 5, rate: 0, series: [] },
max_avail: { latest: 15, rate: 0, series: [] },
rd_bytes: { latest: 6, rate: 4, series: [[0, 2], [1, 6]] }
},
pg_status: { 'active+clean': 8, down: 2 }
}
];
Expand All @@ -215,23 +223,42 @@ describe('PoolListComponent', () => {
cdIsBinary: true,
pg_status: '8 active+clean, 2 down',
stats: {
bytes_used: { latest: 0, rate: 0, series: [] },
max_avail: { latest: 0, rate: 0, series: [] },
bytes_used: { latest: 5, rate: 0, series: [] },
max_avail: { latest: 15, rate: 0, series: [] },
rd: { latest: 0, rate: 0, series: [] },
rd_bytes: { latest: 6, rate: 4, series: [2, 6] },
wr: { latest: 0, rate: 0, series: [] },
wr_bytes: { latest: 0, rate: 0, series: [] }
}
},
usage: 0.25
}
];
expect(component.transformPoolsData(pools)).toEqual(expected);
});

it('transforms pools data correctly if stats are missing', () => {
const pools = [{}];
const expected = [
{
cdIsBinary: true,
pg_status: '',
stats: {
bytes_used: { latest: 0, rate: 0, series: [] },
max_avail: { latest: 0, rate: 0, series: [] },
rd: { latest: 0, rate: 0, series: [] },
rd_bytes: { latest: 0, rate: 0, series: [] },
wr: { latest: 0, rate: 0, series: [] },
wr_bytes: { latest: 0, rate: 0, series: [] }
},
usage: 0
}
];
expect(component.transformPoolsData(pools)).toEqual(expected);
});

it('transforms empty pools data correctly', () => {
const pools = undefined;
const expected = undefined;

expect(component.transformPoolsData(pools)).toEqual(expected);
});
});
Expand Down
Expand Up @@ -23,6 +23,7 @@ import { TaskWrapperService } from '../../../shared/services/task-wrapper.servic
import { URLBuilderService } from '../../../shared/services/url-builder.service';
import { PgCategoryService } from '../../shared/pg-category.service';
import { Pool } from '../pool';
import { PoolStats } from '../pool-stat';

const BASE_URL = 'pool';

Expand Down Expand Up @@ -138,7 +139,12 @@ export class PoolListComponent implements OnInit {
name: this.i18n('Crush Ruleset'),
flexGrow: 3
},
{ name: this.i18n('Usage'), cellTemplate: this.poolUsageTpl, flexGrow: 3 },
{
name: this.i18n('Usage'),
prop: 'usage',
cellTemplate: this.poolUsageTpl,
flexGrow: 3
},
{
prop: 'stats.rd_bytes.series',
name: this.i18n('Read bytes'),
Expand Down Expand Up @@ -213,11 +219,13 @@ export class PoolListComponent implements OnInit {

_.forEach(pools, (pool: Pool) => {
pool['pg_status'] = this.transformPgStatus(pool['pg_status']);
const stats = {};
const stats: PoolStats = {};
_.forEach(requiredStats, (stat) => {
stats[stat] = pool.stats && pool.stats[stat] ? pool.stats[stat] : emptyStat;
});
pool['stats'] = stats;
const avail = stats.bytes_used.latest + stats.max_avail.latest;
pool['usage'] = avail > 0 ? stats.bytes_used.latest / avail : avail;

['rd_bytes', 'wr_bytes'].forEach((stat) => {
pool.stats[stat].series = pool.stats[stat].series.map((point) => point[1]);
Expand Down
Expand Up @@ -3,3 +3,12 @@ export class PoolStat {
rate: number;
series: number[];
}

export class PoolStats {
bytes_used?: PoolStat;
max_avail?: PoolStat;
rd_bytes?: PoolStat;
wr_bytes?: PoolStat;
rd?: PoolStat;
wr?: PoolStat;
}
11 changes: 2 additions & 9 deletions src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.ts
@@ -1,5 +1,5 @@
import { ExecutingTask } from '../../shared/models/executing-task';
import { PoolStat } from './pool-stat';
import { PoolStats } from './pool-stat';

export class Pool {
cache_target_full_ratio_micro: number;
Expand Down Expand Up @@ -58,14 +58,7 @@ export class Pool {
min_write_recency_for_promote: number;
read_tier: number;
pg_status: string;
stats?: {
bytes_used?: PoolStat;
max_avail?: PoolStat;
rd_bytes?: PoolStat;
wr_bytes?: PoolStat;
rd?: PoolStat;
wr?: PoolStat;
};
stats?: PoolStats;
cdIsBinary?: boolean;
configuration: { source: number; name: string; value: string }[];

Expand Down
@@ -1,7 +1,9 @@
import { TableColumn } from '@swimlane/ngx-datatable';
import { TableColumn, TableColumnProp } from '@swimlane/ngx-datatable';

import { CellTemplate } from '../enum/cell-template.enum';

export interface CdTableColumn extends TableColumn {
cellTransformation?: CellTemplate;
isHidden?: boolean;
prop: TableColumnProp; // Enforces properties to get sortable columns
}

0 comments on commit e71c2e6

Please sign in to comment.