From 3fcdbddf2b9e2b49b94b158f03c3e4ae7cd17aa6 Mon Sep 17 00:00:00 2001 From: alfonsomthd Date: Fri, 14 Dec 2018 17:21:42 +0100 Subject: [PATCH] mgr/dashboard: Add info to Pools table MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - Column 'Placement Groups' renamed to 'Pg Status': It shows PG states. - Created 'CephSharedModule' for shared services within ceph module. - Created PgCategoryService & PgCategory model (logic encapsulation). - Color consistency: PG chart (landing page) and cell text color are similar. Fixes: https://tracker.ceph.com/issues/36740 Signed-off-by: Alfonso Martínez --- src/pybind/mgr/dashboard/controllers/pool.py | 2 +- .../app/ceph/dashboard/dashboard.module.ts | 6 +- .../dashboard/health/health.component.spec.ts | 85 +++++++++++++++++-- .../ceph/dashboard/health/health.component.ts | 81 ++++++------------ .../dashboard/pg-status-style.pipe.spec.ts | 24 ------ .../ceph/dashboard/pg-status-style.pipe.ts | 40 --------- .../app/ceph/dashboard/pg-status.pipe.spec.ts | 19 ----- .../src/app/ceph/dashboard/pg-status.pipe.ts | 16 ---- .../pool/pool-list/pool-list.component.scss | 17 ++++ .../pool-list/pool-list.component.spec.ts | 76 ++++++++++++++++- .../pool/pool-list/pool-list.component.ts | 40 +++++++-- .../frontend/src/app/ceph/pool/pool.module.ts | 2 + .../frontend/src/app/ceph/pool/pool.ts | 1 + .../src/app/ceph/shared/ceph-shared.module.ts | 7 ++ .../src/app/ceph/shared/pg-category.model.ts | 71 ++++++++++++++++ .../ceph/shared/pg-category.service.spec.ts | 55 ++++++++++++ .../app/ceph/shared/pg-category.service.ts | 63 ++++++++++++++ .../frontend/src/locale/messages.xlf | 19 ++--- 18 files changed, 439 insertions(+), 185 deletions(-) delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.model.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts diff --git a/src/pybind/mgr/dashboard/controllers/pool.py b/src/pybind/mgr/dashboard/controllers/pool.py index cdd9a7110d4..a0aaee78e16 100644 --- a/src/pybind/mgr/dashboard/controllers/pool.py +++ b/src/pybind/mgr/dashboard/controllers/pool.py @@ -54,7 +54,7 @@ class Pool(RESTController): return [self._serialize_pool(pool, attrs) for pool in pools] - def list(self, attrs=None, stats=False): + def list(self, attrs=None, stats=True): return self._pool_list(attrs, stats) def _get(self, pool_name, attrs=None, stats=False): diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts index e0e4dfe4338..7d751f0362b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts @@ -7,6 +7,7 @@ import { PopoverModule } from 'ngx-bootstrap/popover'; import { TabsModule } from 'ngx-bootstrap/tabs'; import { SharedModule } from '../../shared/shared.module'; +import { CephSharedModule } from '../shared/ceph-shared.module'; import { DashboardComponent } from './dashboard/dashboard.component'; import { HealthPieComponent } from './health-pie/health-pie.component'; import { HealthComponent } from './health/health.component'; @@ -17,11 +18,10 @@ import { MdsSummaryPipe } from './mds-summary.pipe'; import { MgrSummaryPipe } from './mgr-summary.pipe'; import { MonSummaryPipe } from './mon-summary.pipe'; import { OsdSummaryPipe } from './osd-summary.pipe'; -import { PgStatusStylePipe } from './pg-status-style.pipe'; -import { PgStatusPipe } from './pg-status.pipe'; @NgModule({ imports: [ + CephSharedModule, CommonModule, TabsModule.forRoot(), SharedModule, @@ -37,9 +37,7 @@ import { PgStatusPipe } from './pg-status.pipe'; OsdSummaryPipe, LogColorPipe, MgrSummaryPipe, - PgStatusPipe, MdsSummaryPipe, - PgStatusStylePipe, HealthPieComponent, InfoCardComponent, InfoGroupComponent diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts index 847121617ae..bbbbe2b8ed0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts @@ -12,12 +12,13 @@ import { HealthService } from '../../../shared/api/health.service'; import { Permissions } from '../../../shared/models/permissions'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; import { SharedModule } from '../../../shared/shared.module'; +import { PgCategoryService } from '../../shared/pg-category.service'; +import { HealthPieColor } from '../health-pie/health-pie-color.enum'; +import { HealthPieComponent } from '../health-pie/health-pie.component'; import { MdsSummaryPipe } from '../mds-summary.pipe'; import { MgrSummaryPipe } from '../mgr-summary.pipe'; import { MonSummaryPipe } from '../mon-summary.pipe'; import { OsdSummaryPipe } from '../osd-summary.pipe'; -import { PgStatusStylePipe } from '../pg-status-style.pipe'; -import { PgStatusPipe } from '../pg-status.pipe'; import { HealthComponent } from './health.component'; describe('HealthComponent', () => { @@ -49,21 +50,25 @@ describe('HealthComponent', () => { imports: [SharedModule, HttpClientTestingModule, PopoverModule.forRoot()], declarations: [ HealthComponent, + HealthPieComponent, MonSummaryPipe, OsdSummaryPipe, MdsSummaryPipe, - MgrSummaryPipe, - PgStatusStylePipe, - PgStatusPipe + MgrSummaryPipe ], schemas: [NO_ERRORS_SCHEMA], - providers: [i18nProviders, { provide: AuthStorageService, useValue: fakeAuthStorageService }] + providers: [ + i18nProviders, + { provide: AuthStorageService, useValue: fakeAuthStorageService }, + PgCategoryService + ] }); beforeEach(() => { fixture = TestBed.createComponent(HealthComponent); component = fixture.componentInstance; getHealthSpy = spyOn(TestBed.get(HealthService), 'getMinimalHealth'); + getHealthSpy.and.returnValue(of(healthPayload)); }); it('should create', () => { @@ -71,7 +76,6 @@ describe('HealthComponent', () => { }); it('should render all info groups and all info cards', () => { - getHealthSpy.and.returnValue(of(healthPayload)); fixture.detectChanges(); const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group'); @@ -148,7 +152,6 @@ describe('HealthComponent', () => { }); it('should render "Cluster Status" card text that is not clickable', () => { - getHealthSpy.and.returnValue(of(healthPayload)); fixture.detectChanges(); const clusterStatusCard = fixture.debugElement.query( @@ -177,4 +180,70 @@ describe('HealthComponent', () => { const clickableContent = clusterStatusCard.query(By.css('.info-card-content-clickable')); expect(clickableContent.nativeElement.textContent).toEqual(` ${payload.health.status} `); }); + + it('event binding "prepareReadWriteRatio" is called', () => { + const prepareReadWriteRatio = spyOn(component, 'prepareReadWriteRatio'); + + const payload = _.cloneDeep(healthPayload); + payload.client_perf['read_op_per_sec'] = 1; + payload.client_perf['write_op_per_sec'] = 1; + getHealthSpy.and.returnValue(of(payload)); + fixture.detectChanges(); + + expect(prepareReadWriteRatio).toHaveBeenCalled(); + }); + + it('event binding "prepareRawUsage" is called', () => { + const prepareRawUsage = spyOn(component, 'prepareRawUsage'); + + fixture.detectChanges(); + + expect(prepareRawUsage).toHaveBeenCalled(); + }); + + it('event binding "preparePgStatus" is called', () => { + const preparePgStatus = spyOn(component, 'preparePgStatus'); + + fixture.detectChanges(); + + expect(preparePgStatus).toHaveBeenCalled(); + }); + + describe('preparePgStatus', () => { + const expectedChart = (data: number[]) => ({ + colors: [ + { + backgroundColor: [ + HealthPieColor.SHADE_GREEN_CYAN, + HealthPieColor.MEDIUM_DARK_SHADE_CYAN_BLUE, + HealthPieColor.LIGHT_SHADE_BROWN, + HealthPieColor.MEDIUM_LIGHT_SHADE_PINK_RED + ] + } + ], + labels: ['Clean', 'Working', 'Warning', 'Unknown'], + dataset: [{ data: data }] + }); + + it('gets no data', () => { + const chart = { dataset: [{}] }; + component.preparePgStatus(chart, { pg_info: {} }); + expect(chart).toEqual(expectedChart([undefined, undefined, undefined, undefined])); + }); + + it('gets data from all categories', () => { + const chart = { dataset: [{}] }; + component.preparePgStatus(chart, { + pg_info: { + statuses: { + 'clean+active+scrubbing+nonMappedState': 4, + 'clean+active+scrubbing': 2, + 'clean+active': 1, + 'clean+active+scrubbing+down': 3 + } + } + }); + expect(chart).toEqual(expectedChart([1, 2, 3, 4])); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts index 31ce19f494a..bb78e22efa3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts @@ -6,6 +6,8 @@ import * as _ from 'lodash'; import { HealthService } from '../../../shared/api/health.service'; import { Permissions } from '../../../shared/models/permissions'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { PgCategoryService } from '../../shared/pg-category.service'; +import { HealthPieColor } from '../health-pie/health-pie-color.enum'; @Component({ selector: 'cd-health', @@ -20,7 +22,8 @@ export class HealthComponent implements OnInit, OnDestroy { constructor( private healthService: HealthService, private i18n: I18n, - private authStorageService: AuthStorageService + private authStorageService: AuthStorageService, + private pgCategoryService: PgCategoryService ) { this.permissions = this.authStorageService.getPermissions(); } @@ -76,65 +79,35 @@ export class HealthComponent implements OnInit, OnDestroy { } preparePgStatus(chart, data) { - const pgCategoryClean = this.i18n('Clean'); - const pgCategoryCleanStates = ['active', 'clean']; - const pgCategoryWarning = this.i18n('Warning'); - const pgCategoryWarningStates = [ - 'backfill_toofull', - 'backfill_unfound', - 'down', - 'incomplete', - 'inconsistent', - 'recovery_toofull', - 'recovery_unfound', - 'remapped', - 'snaptrim_error', - 'stale', - 'undersized' + const categoryPgAmount = {}; + chart.labels = [ + this.i18n('Clean'), + this.i18n('Working'), + this.i18n('Warning'), + this.i18n('Unknown') ]; - const pgCategoryUnknown = this.i18n('Unknown'); - const pgCategoryWorking = this.i18n('Working'); - const pgCategoryWorkingStates = [ - 'activating', - 'backfill_wait', - 'backfilling', - 'creating', - 'deep', - 'degraded', - 'forced_backfill', - 'forced_recovery', - 'peering', - 'peered', - 'recovering', - 'recovery_wait', - 'repair', - 'scrubbing', - 'snaptrim', - 'snaptrim_wait' + chart.colors = [ + { + backgroundColor: [ + HealthPieColor.SHADE_GREEN_CYAN, + HealthPieColor.MEDIUM_DARK_SHADE_CYAN_BLUE, + HealthPieColor.LIGHT_SHADE_BROWN, + HealthPieColor.MEDIUM_LIGHT_SHADE_PINK_RED + ] + } ]; - let totalPgClean = 0; - let totalPgWarning = 0; - let totalPgUnknown = 0; - let totalPgWorking = 0; _.forEach(data.pg_info.statuses, (pgAmount, pgStatesText) => { - const pgStates = pgStatesText.split('+'); - const isWarning = _.intersection(pgCategoryWarningStates, pgStates).length > 0; - const pgWorkingStates = _.intersection(pgCategoryWorkingStates, pgStates); - const pgCleanStates = _.intersection(pgCategoryCleanStates, pgStates); - - if (isWarning) { - totalPgWarning += pgAmount; - } else if (pgStates.length > pgCleanStates.length + pgWorkingStates.length) { - totalPgUnknown += pgAmount; - } else if (pgWorkingStates.length > 0) { - totalPgWorking = pgAmount; - } else { - totalPgClean += pgAmount; + const categoryType = this.pgCategoryService.getTypeByStates(pgStatesText); + + if (_.isUndefined(categoryPgAmount[categoryType])) { + categoryPgAmount[categoryType] = 0; } + categoryPgAmount[categoryType] += pgAmount; }); - chart.labels = [pgCategoryWarning, pgCategoryClean, pgCategoryUnknown, pgCategoryWorking]; - chart.dataset[0].data = [totalPgWarning, totalPgClean, totalPgUnknown, totalPgWorking]; + chart.dataset[0].data = this.pgCategoryService + .getAllTypes() + .map((categoryType) => categoryPgAmount[categoryType]); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts deleted file mode 100644 index e5c5a098e91..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { PgStatusStylePipe } from './pg-status-style.pipe'; - -describe('PgStatusStylePipe', () => { - const pipe = new PgStatusStylePipe(); - - it('create an instance', () => { - expect(pipe).toBeTruthy(); - }); - - it('transforms with pg status error', () => { - const value = { 'incomplete+clean': 8 }; - expect(pipe.transform(value)).toEqual({ color: '#FF0000' }); - }); - - it('transforms with pg status warning', () => { - const value = { active: 8 }; - expect(pipe.transform(value)).toEqual({ color: '#FFC200' }); - }); - - it('transforms with pg status other', () => { - const value = { 'active+clean': 8 }; - expect(pipe.transform(value)).toEqual({ color: '#00BB00' }); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts deleted file mode 100644 index 4e9afab97cf..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import * as _ from 'lodash'; - -@Pipe({ - name: 'pgStatusStyle' -}) -export class PgStatusStylePipe implements PipeTransform { - transform(pgStatus: any, args?: any): any { - let warning = false; - let error = false; - - _.each(pgStatus, (value, state) => { - if ( - state.includes('inconsistent') || - state.includes('incomplete') || - !state.includes('active') - ) { - error = true; - } - - if ( - state !== 'active+clean' && - state !== 'active+clean+scrubbing' && - state !== 'active+clean+scrubbing+deep' - ) { - warning = true; - } - }); - - if (error) { - return { color: '#FF0000' }; - } - - if (warning) { - return { color: '#FFC200' }; - } - - return { color: '#00BB00' }; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts deleted file mode 100644 index 8b655476658..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { PgStatusPipe } from './pg-status.pipe'; - -describe('PgStatusPipe', () => { - const pipe = new PgStatusPipe(); - - it('create an instance', () => { - expect(pipe).toBeTruthy(); - }); - - it('transforms with 1 status', () => { - const value = { 'active+clean': 8 }; - expect(pipe.transform(value)).toBe('8 active+clean'); - }); - - it('transforms with 2 status', () => { - const value = { active: 8, incomplete: 8 }; - expect(pipe.transform(value)).toBe('8 active, 8 incomplete'); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.ts deleted file mode 100644 index 5c6c7b393c3..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import * as _ from 'lodash'; - -@Pipe({ - name: 'pgStatus' -}) -export class PgStatusPipe implements PipeTransform { - transform(pgStatus: any, args?: any): any { - const strings = []; - _.each(pgStatus, (count, state) => { - strings.push(count + ' ' + state); - }); - - return strings.join(', '); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.scss index e69de29bb2d..e27c9b301e0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.scss @@ -0,0 +1,17 @@ +@import '../../../../defaults'; + +::ng-deep .pg-clean { + color: $color-bright-green; +} + +::ng-deep .pg-working { + color: $color-blue; +} + +::ng-deep .pg-warning { + color: $color-bright-yellow; +} + +::ng-deep .pg-unknown { + color: $color-solid-red; +} 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 c24138b98cf..1e71c63b2ee 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 @@ -14,6 +14,7 @@ import { ExecutingTask } from '../../../shared/models/executing-task'; import { SummaryService } from '../../../shared/services/summary.service'; import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; import { SharedModule } from '../../../shared/shared.module'; +import { PgCategoryService } from '../../shared/pg-category.service'; import { Pool } from '../pool'; import { PoolListComponent } from './pool-list.component'; @@ -31,7 +32,7 @@ describe('PoolListComponent', () => { TabsModule.forRoot(), HttpClientTestingModule ], - providers: i18nProviders + providers: [i18nProviders, PgCategoryService] }); beforeEach(() => { @@ -166,4 +167,77 @@ describe('PoolListComponent', () => { expect(component.pools[2].cdExecuting).toBeFalsy(); }); }); + + describe('getPgStatusCellClass', () => { + const testMethod = (value, expected) => + expect(component.getPgStatusCellClass({ row: '', column: '', value: value })).toEqual({ + 'text-right': true, + [expected]: true + }); + + it('pg-clean', () => { + testMethod('8 active+clean', 'pg-clean'); + }); + + it('pg-working', () => { + testMethod(' 8 active+clean+scrubbing+deep, 255 active+clean ', 'pg-working'); + }); + + it('pg-warning', () => { + testMethod('8 active+clean+scrubbing+down', 'pg-warning'); + testMethod('8 active+clean+scrubbing+down+nonMappedState', 'pg-warning'); + }); + + it('pg-unknown', () => { + testMethod('8 active+clean+scrubbing+nonMappedState', 'pg-unknown'); + testMethod('8 ', 'pg-unknown'); + testMethod('', 'pg-unknown'); + }); + }); + + describe('transformPoolsData', () => { + it('transforms pools data correctly', () => { + const pools = [{ stats: { rate: 0 }, pg_status: { 'active+clean': 8, down: 2 } }]; + const expected = [{ pg_status: '8 active+clean, 2 down' }]; + + expect(component.transformPoolsData(pools)).toEqual(expected); + }); + + it('transforms empty pools data correctly', () => { + const pools = undefined; + const expected = undefined; + + expect(component.transformPoolsData(pools)).toEqual(expected); + }); + }); + + describe('transformPgStatus', () => { + it('returns ststus groups correctly', () => { + const pgStatus = { 'active+clean': 8 }; + const expected = '8 active+clean'; + + expect(component.transformPgStatus(pgStatus)).toEqual(expected); + }); + + it('returns separated status groups', () => { + const pgStatus = { 'active+clean': 8, down: 2 }; + const expected = '8 active+clean, 2 down'; + + expect(component.transformPgStatus(pgStatus)).toEqual(expected); + }); + + it('returns separated statuses correctly', () => { + const pgStatus = { active: 8, down: 2 }; + const expected = '8 active, 2 down'; + + expect(component.transformPgStatus(pgStatus)).toEqual(expected); + }); + + it('returns empty string', () => { + const pgStatus = undefined; + const expected = ''; + + expect(component.transformPgStatus(pgStatus)).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 9cbcefcf675..bf58ce52263 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 @@ -1,6 +1,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { I18n } from '@ngx-translate/i18n-polyfill'; +import * as _ from 'lodash'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { PoolService } from '../../../shared/api/pool.service'; @@ -17,6 +18,7 @@ import { Permissions } from '../../../shared/models/permissions'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; import { TaskListService } from '../../../shared/services/task-list.service'; import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; +import { PgCategoryService } from '../../shared/pg-category.service'; import { Pool } from '../pool'; @Component({ @@ -44,7 +46,8 @@ export class PoolListComponent implements OnInit { private authStorageService: AuthStorageService, private taskListService: TaskListService, private modalService: BsModalService, - private i18n: I18n + private i18n: I18n, + private pgCategoryService: PgCategoryService ) { this.permissions = this.authStorageService.getPermissions(); this.tableActions = [ @@ -85,10 +88,12 @@ export class PoolListComponent implements OnInit { flexGrow: 3 }, { - prop: 'pg_placement_num', - name: this.i18n('Placement Groups'), + prop: 'pg_status', + name: this.i18n('PG Status'), flexGrow: 1, - cellClass: 'text-right' + cellClass: ({ row, column, value }): any => { + return this.getPgStatusCellClass({ row, column, value }); + } }, { prop: 'size', @@ -119,7 +124,7 @@ export class PoolListComponent implements OnInit { this.taskListService.init( () => this.poolService.getList(), undefined, - (pools) => (this.pools = pools), + (pools) => (this.pools = this.transformPoolsData(pools)), () => { this.table.reset(); // Disable loading indicator. this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }]; @@ -147,4 +152,29 @@ export class PoolListComponent implements OnInit { } }); } + + getPgStatusCellClass({ row, column, value }): object { + return { + 'text-right': true, + [`pg-${this.pgCategoryService.getTypeByStates(value)}`]: true + }; + } + + transformPoolsData(pools: any) { + _.map(pools, (pool: object) => { + delete pool['stats']; + pool['pg_status'] = this.transformPgStatus(pool['pg_status']); + }); + + return pools; + } + + transformPgStatus(pgStatus: any): string { + const strings = []; + _.forEach(pgStatus, (count, state) => { + strings.push(`${count} ${state}`); + }); + + return strings.join(', '); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts index d086dd05cc2..46c4fd073ff 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts @@ -9,12 +9,14 @@ import { TabsModule } from 'ngx-bootstrap/tabs'; import { ServicesModule } from '../../shared/services/services.module'; import { SharedModule } from '../../shared/shared.module'; +import { CephSharedModule } from '../shared/ceph-shared.module'; import { ErasureCodeProfileFormComponent } from './erasure-code-profile-form/erasure-code-profile-form.component'; import { PoolFormComponent } from './pool-form/pool-form.component'; import { PoolListComponent } from './pool-list/pool-list.component'; @NgModule({ imports: [ + CephSharedModule, CommonModule, TabsModule, PopoverModule.forRoot(), 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 cccab137439..4241565d8b5 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 @@ -60,6 +60,7 @@ export class Pool { last_change: string; min_write_recency_for_promote: number; read_tier: number; + pg_status: string; constructor(name) { this.pool_name = name; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts new file mode 100644 index 00000000000..aaf0ddcf7b9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts @@ -0,0 +1,7 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +@NgModule({ + imports: [CommonModule] +}) +export class CephSharedModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.model.ts new file mode 100644 index 00000000000..12fda778405 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.model.ts @@ -0,0 +1,71 @@ +export class PgCategory { + static readonly CATEGORY_CLEAN = 'clean'; + static readonly CATEGORY_WORKING = 'working'; + static readonly CATEGORY_WARNING = 'warning'; + static readonly CATEGORY_UNKNOWN = 'unknown'; + static readonly VALID_CATEGORIES = [ + PgCategory.CATEGORY_CLEAN, + PgCategory.CATEGORY_WORKING, + PgCategory.CATEGORY_WARNING, + PgCategory.CATEGORY_UNKNOWN + ]; + + states: string[]; + + constructor(public type: string) { + if (!this.isValidType()) { + throw new Error('Wrong placement group category type'); + } + + this.setTypeStates(); + } + + private isValidType() { + return PgCategory.VALID_CATEGORIES.includes(this.type); + } + + private setTypeStates() { + switch (this.type) { + case PgCategory.CATEGORY_CLEAN: + this.states = ['active', 'clean']; + break; + case PgCategory.CATEGORY_WORKING: + this.states = [ + 'activating', + 'backfill_wait', + 'backfilling', + 'creating', + 'deep', + 'degraded', + 'forced_backfill', + 'forced_recovery', + 'peering', + 'peered', + 'recovering', + 'recovery_wait', + 'repair', + 'scrubbing', + 'snaptrim', + 'snaptrim_wait' + ]; + break; + case PgCategory.CATEGORY_WARNING: + this.states = [ + 'backfill_toofull', + 'backfill_unfound', + 'down', + 'incomplete', + 'inconsistent', + 'recovery_toofull', + 'recovery_unfound', + 'remapped', + 'snaptrim_error', + 'stale', + 'undersized' + ]; + break; + default: + this.states = []; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.spec.ts new file mode 100644 index 00000000000..5a71c208c08 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.spec.ts @@ -0,0 +1,55 @@ +import { TestBed } from '@angular/core/testing'; + +import { configureTestBed } from '../../../testing/unit-test-helper'; +import { PgCategory } from './pg-category.model'; +import { PgCategoryService } from './pg-category.service'; + +describe('PgCategoryService', () => { + let service: PgCategoryService; + + configureTestBed({ + providers: [PgCategoryService] + }); + + beforeEach(() => { + service = TestBed.get(PgCategoryService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('returns all category types', () => { + const categoryTypes = service.getAllTypes(); + + expect(categoryTypes).toEqual(PgCategory.VALID_CATEGORIES); + }); + + describe('getTypeByStates', () => { + const testMethod = (value, expected) => + expect(service.getTypeByStates(value)).toEqual(expected); + + it(PgCategory.CATEGORY_CLEAN, () => { + testMethod('clean', PgCategory.CATEGORY_CLEAN); + }); + + it(PgCategory.CATEGORY_WORKING, () => { + testMethod('clean+scrubbing', PgCategory.CATEGORY_WORKING); + testMethod( + ' 8 active+clean+scrubbing+deep, 255 active+clean ', + PgCategory.CATEGORY_WORKING + ); + }); + + it(PgCategory.CATEGORY_WARNING, () => { + testMethod('clean+scrubbing+down', PgCategory.CATEGORY_WARNING); + testMethod('clean+scrubbing+down+nonMappedState', PgCategory.CATEGORY_WARNING); + }); + + it(PgCategory.CATEGORY_UNKNOWN, () => { + testMethod('clean+scrubbing+nonMappedState', PgCategory.CATEGORY_UNKNOWN); + testMethod('nonMappedState', PgCategory.CATEGORY_UNKNOWN); + testMethod('', PgCategory.CATEGORY_UNKNOWN); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts new file mode 100644 index 00000000000..79c2bcc984b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; + +import * as _ from 'lodash'; + +import { CephSharedModule } from './ceph-shared.module'; +import { PgCategory } from './pg-category.model'; + +@Injectable({ + providedIn: CephSharedModule +}) +export class PgCategoryService { + private categories: object; + + constructor() { + this.categories = this.createCategories(); + } + + getAllTypes() { + return PgCategory.VALID_CATEGORIES; + } + + getTypeByStates(pgStatesText: string): string { + const pgStates = this.getPgStatesFromText(pgStatesText); + + if (pgStates.length === 0) { + return PgCategory.CATEGORY_UNKNOWN; + } + + const intersections = _.zipObject( + PgCategory.VALID_CATEGORIES, + PgCategory.VALID_CATEGORIES.map( + (category) => _.intersection(this.categories[category].states, pgStates).length + ) + ); + + if (intersections[PgCategory.CATEGORY_WARNING] > 0) { + return PgCategory.CATEGORY_WARNING; + } + + const pgWorkingStates = intersections[PgCategory.CATEGORY_WORKING]; + if (pgStates.length > intersections[PgCategory.CATEGORY_CLEAN] + pgWorkingStates) { + return PgCategory.CATEGORY_UNKNOWN; + } + + return pgWorkingStates ? PgCategory.CATEGORY_WORKING : PgCategory.CATEGORY_CLEAN; + } + + private createCategories() { + return _.zipObject( + PgCategory.VALID_CATEGORIES, + PgCategory.VALID_CATEGORIES.map((category) => new PgCategory(category)) + ); + } + + private getPgStatesFromText(pgStatesText) { + const pgStates = pgStatesText + .replace(/[^a-z]+/g, ' ') + .trim() + .split(' '); + + return _.uniq(pgStates); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf index d308849f5fc..10caf93c623 100644 --- a/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf +++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf @@ -3970,22 +3970,22 @@ 1 - - Warning + + Working src/app/ceph/dashboard/health/health.component.ts 1 - - Unknown + + Warning src/app/ceph/dashboard/health/health.component.ts 1 - - Working + + Unknown src/app/ceph/dashboard/health/health.component.ts 1 @@ -4119,13 +4119,6 @@ 1 - - Placement Groups - - src/app/ceph/pool/pool-list/pool-list.component.ts - 1 - - Replica Size -- 2.39.5