]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: display placement column in service table 38336/head
authorVolker Theile <vtheile@suse.com>
Tue, 5 Jan 2021 10:57:15 +0000 (11:57 +0100)
committerVolker Theile <vtheile@suse.com>
Mon, 25 Jan 2021 09:24:56 +0000 (10:24 +0100)
Fixes: https://tracker.ceph.com/issues/44404
Signed-off-by: Volker Theile <vtheile@suse.com>
(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/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts

index d380f42197fe89375d536733e34b84731d6f8b6e..85eff4f30aa604a73bda1eb5bd3f705197dcf3e2 100644 (file)
@@ -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 (file)
index 0000000..7db2d14
--- /dev/null
@@ -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 (file)
index 0000000..3114b1c
--- /dev/null
@@ -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<any> = [];
+    const hosts: Array<string> = _.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(';');
+  }
+}
index 37369d68a4efe16c04861fc71e151e40a0f39acc..297db08e8c56ae02b4b5bdc7296037a1f08c2293 100644 (file)
@@ -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();
   });
index b27647e5bc8010759023450acca4e6f90e941c36..cd977bc714e63a1f6beafa118a17cc5de4233cd4 100644 (file)
@@ -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',