From 36f5ea6de187ebb53d2c99b81e79b8512ccd9ed2 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Tue, 5 Jan 2021 11:57:15 +0100 Subject: [PATCH] mgr/dashboard: display placement column in service table Fixes: https://tracker.ceph.com/issues/44404 Signed-off-by: Volker Theile (cherry picked from commit 1c722aa89ec1efbf5cc76ea968a1f9a725a86e57) Conflicts: src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.ts Both files need to be adapted to replaced $localize with i18n. --- .../src/app/ceph/cluster/cluster.module.ts | 4 +- .../cluster/services/placement.pipe.spec.ts | 92 +++++++++++++++++++ .../ceph/cluster/services/placement.pipe.ts | 44 +++++++++ .../services/services.component.spec.ts | 3 + .../cluster/services/services.component.ts | 7 ++ 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index d380f42197fe..85eff4f30aa6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -49,6 +49,7 @@ import { RulesListComponent } from './prometheus/rules-list/rules-list.component import { SilenceFormComponent } from './prometheus/silence-form/silence-form.component'; import { SilenceListComponent } from './prometheus/silence-list/silence-list.component'; import { SilenceMatcherModalComponent } from './prometheus/silence-matcher-modal/silence-matcher-modal.component'; +import { PlacementPipe } from './services/placement.pipe'; import { ServiceDaemonListComponent } from './services/service-daemon-list/service-daemon-list.component'; import { ServiceDetailsComponent } from './services/service-details/service-details.component'; import { ServiceFormComponent } from './services/service-form/service-form.component'; @@ -129,7 +130,8 @@ import { TelemetryComponent } from './telemetry/telemetry.component'; ServiceDaemonListComponent, TelemetryComponent, OsdFlagsIndivModalComponent, - ServiceFormComponent + ServiceFormComponent, + PlacementPipe ] }) export class ClusterModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.spec.ts new file mode 100644 index 000000000000..7db2d14f9171 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.spec.ts @@ -0,0 +1,92 @@ +import { TestBed } from '@angular/core/testing'; +import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; + +import { PlacementPipe } from './placement.pipe'; + +describe('PlacementPipe', () => { + let pipe: PlacementPipe; + + configureTestBed({ + providers: [i18nProviders] + }); + + beforeEach(() => { + const i18n = TestBed.get(I18n); + pipe = new PlacementPipe(i18n); + }); + + it('create an instance', () => { + expect(pipe).toBeTruthy(); + }); + + it('transforms to no spec', () => { + expect(pipe.transform(undefined)).toBe('no spec'); + }); + + it('transforms to unmanaged', () => { + expect(pipe.transform({ unmanaged: true })).toBe('unmanaged'); + }); + + it('transforms placement (1)', () => { + expect( + pipe.transform({ + placement: { + hosts: ['mon0'] + } + }) + ).toBe('mon0'); + }); + + it('transforms placement (2)', () => { + expect( + pipe.transform({ + placement: { + hosts: ['mon0', 'mgr0'] + } + }) + ).toBe('mon0;mgr0'); + }); + + it('transforms placement (3)', () => { + expect( + pipe.transform({ + placement: { + count: 1 + } + }) + ).toBe('count:1'); + }); + + it('transforms placement (4)', () => { + expect( + pipe.transform({ + placement: { + label: 'foo' + } + }) + ).toBe('label:foo'); + }); + + it('transforms placement (5)', () => { + expect( + pipe.transform({ + placement: { + host_pattern: '*' + } + }) + ).toBe('*'); + }); + + it('transforms placement (6)', () => { + expect( + pipe.transform({ + placement: { + count: 2, + hosts: ['mon0', 'mgr0'] + } + }) + ).toBe('mon0;mgr0;count:2'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.ts new file mode 100644 index 000000000000..3114b1c15ac4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.ts @@ -0,0 +1,44 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import * as _ from 'lodash'; + +@Pipe({ + name: 'placement' +}) +export class PlacementPipe implements PipeTransform { + constructor(private i18n: I18n) {} + + /** + * Convert the placement configuration into human readable form. + * The output is equal to the column 'PLACEMENT' in 'ceph orch ls'. + * @param serviceSpec The service specification to process. + * @return The placement configuration as human readable string. + */ + transform(serviceSpec: object | undefined): string { + if (_.isUndefined(serviceSpec)) { + return this.i18n('no spec'); + } + if (_.get(serviceSpec, 'unmanaged', false)) { + return this.i18n('unmanaged'); + } + const kv: Array = []; + const hosts: Array = _.get(serviceSpec, 'placement.hosts'); + const count: number = _.get(serviceSpec, 'placement.count'); + const label: string = _.get(serviceSpec, 'placement.label'); + const hostPattern: string = _.get(serviceSpec, 'placement.host_pattern'); + if (_.isArray(hosts)) { + kv.push(...hosts); + } + if (_.isNumber(count)) { + kv.push(this.i18n('count:{{count}}', { count })); + } + if (_.isString(label)) { + kv.push(this.i18n('label:{{label}}', { label })); + } + if (_.isString(hostPattern)) { + kv.push(...hostPattern); + } + return kv.join(';'); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts index 37369d68a4ef..297db08e8c56 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts @@ -83,7 +83,10 @@ describe('ServicesComponent', () => { it('should have columns that are sortable', () => { expect( component.columns + // Filter the 'Expand/Collapse Row' column. .filter((column) => !(column.cellClass === 'cd-datatable-expand-collapse')) + // Filter the 'Placement' column. + .filter((column) => !(column.prop === '')) .every((column) => Boolean(column.prop)) ).toBeTruthy(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts index b27647e5bc80..cd977bc714e6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts @@ -22,6 +22,7 @@ import { CephServiceSpec } from '../../../shared/models/service.interface'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; import { URLBuilderService } from '../../../shared/services/url-builder.service'; +import { PlacementPipe } from './placement.pipe'; const BASE_URL = 'services'; @@ -103,6 +104,12 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI length: 12 } }, + { + name: this.i18n('Placement'), + prop: '', + pipe: new PlacementPipe(this.i18n), + flexGrow: 1 + }, { name: this.i18n('Running'), prop: 'status.running', -- 2.47.3