]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Make all columns sortable 27889/head
authorStephan Müller <smueller@suse.com>
Thu, 25 Apr 2019 12:34:00 +0000 (14:34 +0200)
committerStephan Müller <smueller@suse.com>
Tue, 30 Apr 2019 12:33:59 +0000 (14:33 +0200)
Fixes: https://tracker.ceph.com/issues/39483
Signed-off-by: Stephan Müller <smueller@suse.com>
(cherry picked from commit ba6d0f96dbdd2ccb4824a6377e741a9534d46287)

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-stat.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts

index d775fb0208f89239b217b6f14a3b7f352dad92df..5163bb6b70850f23b0a131189f71f467c65ed562 100644 (file)
@@ -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<OsdListComponent>;
   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;
index f3ecc673bc0226745f324470c93e2f841e16e94a..7fe5abd41b0cee2058b16a61819d585a44170b47 100644 (file)
@@ -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;
       });
index 4a219d8b10390a5d40f7e5e77fbd3213c221ab0e..769dde5832441e82eaeae215bb5050d6d79c73eb 100644 (file)
@@ -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(() => {});
 
index 3d5979582d18d532067793d54c42749abadc79d3..ac6ea523576a1f87e9b04f235c5c000a4ef108bc 100644 (file)
@@ -49,6 +49,7 @@ export class TablePerformanceCounterComponent implements OnInit {
       },
       {
         name: this.i18n('Value'),
+        prop: 'value',
         cellTemplate: this.valueTpl,
         flexGrow: 1
       }
index da7161dd4dce32c8ab7200351d0acabfcca342a8..21e553ba14721bdb0a0dd6e891e948d990c47c37 100644 (file)
@@ -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);
     });
   });
index c3495d5cf1b8e72a24f35a3cdce27c3beb6df8fe..91e9b0ec2a8c89706e6fc09a64b3f4ace7454da4 100644 (file)
@@ -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]);
index 66b5852131cf6688210221d943cf951bb19b6bfa..ea2ea0b215edfb06e967f355f85ed1fb19d02d6b 100644 (file)
@@ -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;
+}
index 17be0f1d8e1a8d1299c799468326aed95de7de8a..013718d7ffd379a51c5c8bf1e58d496c0041c161 100644 (file)
@@ -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 }[];
 
index bf45c48187485d0646c31f4d26a8fbdcfddf4f47..69194e8b88d77f49fd386f5823fd682cc630db12 100644 (file)
@@ -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
 }