From ba6d0f96dbdd2ccb4824a6377e741a9534d46287 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Stephan=20M=C3=BCller?= Date: Thu, 25 Apr 2019 14:34:00 +0200 Subject: [PATCH] mgr/dashboard: Make all columns sortable MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Fixes: https://tracker.ceph.com/issues/39483 Signed-off-by: Stephan Müller --- .../osd/osd-list/osd-list.component.spec.ts | 76 ++++++++++++++++++- .../osd/osd-list/osd-list.component.ts | 6 +- ...able-performance-counter.component.spec.ts | 4 + .../table-performance-counter.component.ts | 1 + .../pool-list/pool-list.component.spec.ts | 37 +++++++-- .../pool/pool-list/pool-list.component.ts | 12 ++- .../frontend/src/app/ceph/pool/pool-stat.ts | 9 +++ .../frontend/src/app/ceph/pool/pool.ts | 11 +-- .../src/app/shared/models/cd-table-column.ts | 4 +- 9 files changed, 136 insertions(+), 24 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts index d775fb0208f8..5163bb6b7085 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts @@ -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'; @@ -32,6 +33,7 @@ describe('OsdListComponent', () => { let component: OsdListComponent; let fixture: ComponentFixture; let modalServiceShowSpy: jasmine.Spy; + let osdService: OsdService; const fakeAuthStorageService = { getPermissions: () => { @@ -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(); }); @@ -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 }; @@ -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; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts index f3ecc673bc02..7fe5abd41b0c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts @@ -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'), @@ -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; }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts index 4a219d8b1039..769dde583244 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts @@ -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(() => {}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts index 3d5979582d18..ac6ea523576a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts @@ -49,6 +49,7 @@ export class TablePerformanceCounterComponent implements OnInit { }, { name: this.i18n('Value'), + prop: 'value', cellTemplate: this.valueTpl, flexGrow: 1 } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts index da7161dd4dce..21e553ba1472 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts @@ -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; @@ -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 } } ]; @@ -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); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts index c3495d5cf1b8..91e9b0ec2a8c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts @@ -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'; @@ -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'), @@ -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]); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-stat.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-stat.ts index 66b5852131cf..ea2ea0b215ed 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-stat.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-stat.ts @@ -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; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.ts index 17be0f1d8e1a..013718d7ffd3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.ts +++ b/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; @@ -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 }[]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts index bf45c4818748..69194e8b88d7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts @@ -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 } -- 2.47.3