]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: show degraded/misplaced/unfound objects. 28104/head
authoralfonsomthd <almartin@redhat.com>
Thu, 30 May 2019 07:57:54 +0000 (09:57 +0200)
committeralfonsomthd <almartin@redhat.com>
Thu, 30 May 2019 07:57:54 +0000 (09:57 +0200)
* Landing Page 'Objects' card now is a chart that shows more info about objects.
* Fix: Dimless/dimlessBinary pipe applied to amount displayed in
  chart slice tooltip body (if shown).
* Refactoring: simplified way of setting chart initial config
  via 'config' @Input; erased redundant @Inputs.
  Updated chart component default config (for the sake of simplicity).

Fixes: https://tracker.ceph.com/issues/39613
Signed-off-by: Alfonso Martínez <almartin@redhat.com>
qa/tasks/mgr/dashboard/test_health.py
src/pybind/mgr/dashboard/controllers/health.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts
src/pybind/mgr/dashboard/services/ceph_service.py

index 71301cb5dbad9b7fc389d65d62a3fda9c9d0c2b6..8e47a280c5bab5e0ecb6c6d1cbe535e2ed07fca8 100644 (file)
@@ -7,6 +7,18 @@ from .helper import DashboardTestCase, JAny, JLeaf, JList, JObj
 class HealthTest(DashboardTestCase):
     CEPHFS = True
 
+    __pg_info_schema = JObj({
+        'object_stats': JObj({
+            'num_objects': int,
+            'num_object_copies': int,
+            'num_objects_degraded': int,
+            'num_objects_misplaced': int,
+            'num_objects_unfound': int
+        }),
+        'pgs_per_osd': float,
+        'statuses': JObj({}, allow_unknown=True, unknown_schema=int)
+    })
+
     def test_minimal_health(self):
         data = self._get('/api/health/minimal')
         self.assertStatus(200)
@@ -22,7 +34,6 @@ class HealthTest(DashboardTestCase):
                 'stats': JObj({
                     'total_avail_bytes': int,
                     'total_bytes': int,
-                    'total_objects': int,
                     'total_used_raw_bytes': int,
                 })
             }),
@@ -65,10 +76,7 @@ class HealthTest(DashboardTestCase):
                         'up': int,
                     })),
             }),
-            'pg_info': JObj({
-                'pgs_per_osd': float,
-                'statuses': JObj({}, allow_unknown=True, unknown_schema=int)
-            }),
+            'pg_info': self.__pg_info_schema,
             'pools': JList(JLeaf(dict)),
             'rgw': int,
             'scrub_status': str
@@ -134,7 +142,6 @@ class HealthTest(DashboardTestCase):
                 'stats': JObj({
                     'total_avail_bytes': int,
                     'total_bytes': int,
-                    'total_objects': int,
                     'total_used_bytes': int,
                     'total_used_raw_bytes': int,
                     'total_used_raw_ratio': float
@@ -243,10 +250,7 @@ class HealthTest(DashboardTestCase):
                         'up': int,
                     }, allow_unknown=True)),
             }, allow_unknown=True),
-            'pg_info': JObj({
-                'pgs_per_osd': float,
-                'statuses': JObj({}, allow_unknown=True, unknown_schema=int)
-            }),
+            'pg_info': self.__pg_info_schema,
             'pools': JList(JLeaf(dict)),
             'rgw': int,
             'scrub_status': str
index eaff5be7173ef4fc8961c51cbe30bf35e7ce398f..d2b232a6afa4292c4381ddbf9dd1105709717f1f 100644 (file)
@@ -94,12 +94,10 @@ class HealthData(object):
 
         del df['stats_by_class']
 
-        df['stats']['total_objects'] = sum(
-            [p['stats']['objects'] for p in df['pools']])
         if self._minimal:
             df = dict(stats=self._partial_dict(
                 df['stats'],
-                ['total_avail_bytes', 'total_bytes', 'total_objects',
+                ['total_avail_bytes', 'total_bytes',
                  'total_used_raw_bytes']
             ))
         return df
@@ -163,10 +161,7 @@ class HealthData(object):
         return osd_map
 
     def pg_info(self):
-        pg_info = CephService.get_pg_info()
-        if self._minimal:
-            pg_info = self._partial_dict(pg_info, ['pgs_per_osd', 'statuses'])
-        return pg_info
+        return CephService.get_pg_info()
 
     def pools(self):
         pools = CephService.get_pool_list_with_stats()
index 88f7be522911aad92ef8482d317e5dcc0f729784..25782502ca55b8d8fdd042a45ddda84c0d2bb29c 100644 (file)
@@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { configureTestBed } from '../../../../testing/unit-test-helper';
 import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
 import { FormatterService } from '../../../shared/services/formatter.service';
 import { HealthPieComponent } from './health-pie.component';
 
@@ -13,7 +14,7 @@ describe('HealthPieComponent', () => {
   configureTestBed({
     schemas: [NO_ERRORS_SCHEMA],
     declarations: [HealthPieComponent],
-    providers: [DimlessBinaryPipe, FormatterService]
+    providers: [DimlessBinaryPipe, DimlessPipe, FormatterService]
   });
 
   beforeEach(() => {
@@ -25,34 +26,6 @@ describe('HealthPieComponent', () => {
     expect(component).toBeTruthy();
   });
 
-  it('Set doughnut if nothing received', () => {
-    component.chartType = '';
-    fixture.detectChanges();
-
-    expect(component.chartConfig.chartType).toEqual('doughnut');
-  });
-
-  it('Set doughnut if not allowed value received', () => {
-    component.chartType = 'badType';
-    fixture.detectChanges();
-
-    expect(component.chartConfig.chartType).toEqual('doughnut');
-  });
-
-  it('Set doughnut if doughnut received', () => {
-    component.chartType = 'doughnut';
-    fixture.detectChanges();
-
-    expect(component.chartConfig.chartType).toEqual('doughnut');
-  });
-
-  it('Set pie if pie received', () => {
-    component.chartType = 'pie';
-    fixture.detectChanges();
-
-    expect(component.chartConfig.chartType).toEqual('pie');
-  });
-
   it('Add slice border if there is more than one slice with numeric non zero value', () => {
     component.chartConfig.dataset[0].data = [48, 0, 1, 0];
     component.ngOnChanges();
@@ -81,4 +54,21 @@ describe('HealthPieComponent', () => {
 
     expect(component.chartConfig.dataset[0].data).toEqual(initialData);
   });
+
+  describe('tooltip body', () => {
+    const tooltipBody = ['text: 10000'];
+
+    it('should return amount converted to appropriate units', () => {
+      component.isBytesData = false;
+      expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text: 10 k');
+
+      component.isBytesData = true;
+      expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text: 9.8 KiB');
+    });
+
+    it('should not return amount when showing label as tooltip', () => {
+      component.showLabelAsTooltip = true;
+      expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text');
+    });
+  });
 });
index 1016254c562853e220a508de530c991c295a7832..f4baefb494b377f90466247bcada8743f488d25e 100644 (file)
@@ -14,6 +14,7 @@ import * as _ from 'lodash';
 
 import { ChartTooltip } from '../../../shared/models/chart-tooltip';
 import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
 import { HealthPieColor } from './health-pie-color.enum';
 
 @Component({
@@ -30,12 +31,10 @@ export class HealthPieComponent implements OnChanges, OnInit {
   @Input()
   data: any;
   @Input()
-  chartType: string;
+  config = {};
   @Input()
   isBytesData = false;
   @Input()
-  displayLegend = false;
-  @Input()
   tooltipFn: any;
   @Input()
   showLabelAsTooltip = false;
@@ -43,6 +42,7 @@ export class HealthPieComponent implements OnChanges, OnInit {
   prepareFn = new EventEmitter();
 
   chartConfig: any = {
+    chartType: 'pie',
     dataset: [
       {
         label: null,
@@ -51,7 +51,7 @@ export class HealthPieComponent implements OnChanges, OnInit {
     ],
     options: {
       legend: {
-        display: false,
+        display: true,
         position: 'right',
         labels: { usePointStyle: true },
         onClick: (event, legendItem) => {
@@ -59,15 +59,17 @@ export class HealthPieComponent implements OnChanges, OnInit {
         }
       },
       animation: { duration: 0 },
-
       tooltips: {
         enabled: false
+      },
+      title: {
+        display: false
       }
     }
   };
   private hiddenSlices = [];
 
-  constructor(private dimlessBinary: DimlessBinaryPipe) {}
+  constructor(private dimlessBinary: DimlessBinaryPipe, private dimless: DimlessPipe) {}
 
   ngOnInit() {
     // An extension to Chart.js to enable rendering some
@@ -121,10 +123,6 @@ export class HealthPieComponent implements OnChanges, OnInit {
       chartTooltip.customTooltips(tooltip);
     };
 
-    this.setChartType();
-
-    this.chartConfig.options.legend.display = this.displayLegend;
-
     this.chartConfig.colors = [
       {
         backgroundColor: [
@@ -137,6 +135,8 @@ export class HealthPieComponent implements OnChanges, OnInit {
       }
     ];
 
+    _.merge(this.chartConfig, this.config);
+
     this.prepareFn.emit([this.chartConfig, this.data]);
   }
 
@@ -153,24 +153,13 @@ export class HealthPieComponent implements OnChanges, OnInit {
       return bodySplit[0];
     }
 
-    if (this.isBytesData) {
-      bodySplit[1] = this.dimlessBinary.transform(bodySplit[1]);
-    }
+    bodySplit[1] = this.isBytesData
+      ? this.dimlessBinary.transform(bodySplit[1])
+      : this.dimless.transform(bodySplit[1]);
 
     return bodySplit.join(': ');
   }
 
-  private setChartType() {
-    const chartTypes = ['doughnut', 'pie'];
-    const selectedChartType = chartTypes.find((chartType) => chartType === this.chartType);
-
-    if (selectedChartType !== undefined) {
-      this.chartConfig.chartType = selectedChartType;
-    } else {
-      this.chartConfig.chartType = chartTypes[0];
-    }
-  }
-
   private setChartSliceBorderWidth() {
     let nonZeroValueSlices = 0;
     _.forEach(this.chartConfig.dataset[0].data, function(slice) {
index 592c14b8d8c466d781b6ec23c498c7e97886e809..a2f7244b619c6dc178fbd943cb7acc7e3c73af79 100644 (file)
                     *ngIf="healthData.client_perf">
         <cd-health-pie *ngIf="isClientReadWriteChartShowable()"
                        [data]="healthData"
-                       [isBytesData]="false"
-                       chartType="pie"
-                       [displayLegend]="true"
                        (prepareFn)="prepareReadWriteRatio($event[0], $event[1])">
         </cd-health-pie>
         <span *ngIf="!isClientReadWriteChartShowable()">
                  class="row info-group"
                  *ngIf="healthData.pools
                  || healthData.df
-                 || healthData.df?.stats?.total_objects != null
                  || healthData.pg_info">
 
     <div class="cd-container-flex">
                     contentClass="content-chart"
                     *ngIf="healthData.df">
         <cd-health-pie [data]="healthData"
+                       [config]="rawCapacityChartConfig"
                        [showLabelAsTooltip]="true"
-                       chartType="pie"
-                       [displayLegend]="true"
                        (prepareFn)="prepareRawUsage($event[0], $event[1])">
         </cd-health-pie>
       </cd-info-card>
                     i18n-cardTitle
                     class="cd-col-5"
                     cardClass="card-medium"
-                    contentClass="content-medium content-highlight"
-                    *ngIf="healthData.df?.stats?.total_objects != null">
-        {{ healthData.df?.stats?.total_objects }}
+                    contentClass="content-chart"
+                    *ngIf="healthData.pg_info?.object_stats?.num_objects != null">
+        <cd-health-pie [data]="healthData"
+                       [config]="objectsChartConfig"
+                       (prepareFn)="prepareObjects($event[0], $event[1])">
+        </cd-health-pie>
       </cd-info-card>
 
       <cd-info-card cardTitle="PGs per OSD"
                #pgStatusTarget="bs-popover"
                placement="bottom">
             <cd-health-pie [data]="healthData"
-                           chartType="pie"
-                           [displayLegend]="true"
+                           [config]="pgStatusChartConfig"
                            (prepareFn)="preparePgStatus($event[0], $event[1])">
             </cd-health-pie>
           </div>
index b4823fc8485d3ef1a10245aed67978cedbb01cf8..8c89000ca37d7237788f628aaf15ac4b543f60c8 100644 (file)
@@ -15,7 +15,6 @@ import { FeatureTogglesService } from '../../../shared/services/feature-toggles.
 import { RefreshIntervalService } from '../../../shared/services/refresh-interval.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';
@@ -39,8 +38,8 @@ describe('HealthComponent', () => {
     client_perf: {},
     scrub_status: 'Inactive',
     pools: [],
-    df: { stats: { total_objects: 0 } },
-    pg_info: {}
+    df: { stats: {} },
+    pg_info: { object_stats: { num_objects: 0 } }
   };
   const fakeAuthStorageService = {
     getPermissions: () => {
@@ -248,20 +247,18 @@ describe('HealthComponent', () => {
     expect(preparePgStatus).toHaveBeenCalled();
   });
 
+  it('event binding "prepareObjects" is called', () => {
+    const prepareObjects = spyOn(component, 'prepareObjects');
+
+    fixture.detectChanges();
+
+    expect(prepareObjects).toHaveBeenCalled();
+  });
+
   describe('preparePgStatus', () => {
     const calcPercentage = (data) => Math.round((data / 10) * 100) || 0;
 
     const expectedChart = (data: number[]) => ({
-      colors: [
-        {
-          backgroundColor: [
-            HealthPieColor.DEFAULT_GREEN,
-            HealthPieColor.DEFAULT_BLUE,
-            HealthPieColor.DEFAULT_ORANGE,
-            HealthPieColor.DEFAULT_RED
-          ]
-        }
-      ],
       labels: [
         `Clean (${calcPercentage(data[0])}%)`,
         `Working (${calcPercentage(data[1])}%)`,
@@ -326,4 +323,17 @@ describe('HealthComponent', () => {
       expect(component.isClientReadWriteChartShowable()).toBeTruthy();
     });
   });
+
+  describe('calcPercentage', () => {
+    it('returns correct value', () => {
+      expect(component['calcPercentage'](1, undefined)).toEqual(0);
+      expect(component['calcPercentage'](1, null)).toEqual(0);
+      expect(component['calcPercentage'](1, 0)).toEqual(0);
+      expect(component['calcPercentage'](undefined, 1)).toEqual(0);
+      expect(component['calcPercentage'](null, 1)).toEqual(0);
+      expect(component['calcPercentage'](0, 1)).toEqual(0);
+      expect(component['calcPercentage'](2.346, 10)).toEqual(23);
+      expect(component['calcPercentage'](2.35, 10)).toEqual(24);
+    });
+  });
 });
index d43024bdcfd335e091d5f1cbaf8f2ac009795384..6cca3b5cac495862d4e0960c1deca8eda5e91fd8 100644 (file)
@@ -7,6 +7,7 @@ import { Subscription } from 'rxjs/Subscription';
 import { HealthService } from '../../../shared/api/health.service';
 import { Permissions } from '../../../shared/models/permissions';
 import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
 import {
   FeatureTogglesMap$,
@@ -27,6 +28,39 @@ export class HealthComponent implements OnInit, OnDestroy {
   permissions: Permissions;
   enabledFeature$: FeatureTogglesMap$;
 
+  rawCapacityChartConfig = {
+    options: {
+      title: { display: true, position: 'bottom' }
+    }
+  };
+  objectsChartConfig = {
+    options: {
+      title: { display: true, position: 'bottom' }
+    },
+    colors: [
+      {
+        backgroundColor: [
+          HealthPieColor.DEFAULT_GREEN,
+          HealthPieColor.DEFAULT_MAGENTA,
+          HealthPieColor.DEFAULT_ORANGE,
+          HealthPieColor.DEFAULT_RED
+        ]
+      }
+    ]
+  };
+  pgStatusChartConfig = {
+    colors: [
+      {
+        backgroundColor: [
+          HealthPieColor.DEFAULT_GREEN,
+          HealthPieColor.DEFAULT_BLUE,
+          HealthPieColor.DEFAULT_ORANGE,
+          HealthPieColor.DEFAULT_RED
+        ]
+      }
+    ]
+  };
+
   constructor(
     private healthService: HealthService,
     private i18n: I18n,
@@ -34,7 +68,8 @@ export class HealthComponent implements OnInit, OnDestroy {
     private pgCategoryService: PgCategoryService,
     private featureToggles: FeatureTogglesService,
     private refreshIntervalService: RefreshIntervalService,
-    private dimlessBinary: DimlessBinaryPipe
+    private dimlessBinary: DimlessBinaryPipe,
+    private dimless: DimlessPipe
   ) {
     this.permissions = this.authStorageService.getPermissions();
     this.enabledFeature$ = this.featureToggles.get();
@@ -63,12 +98,20 @@ export class HealthComponent implements OnInit, OnDestroy {
 
     const total =
       this.healthData.client_perf.write_op_per_sec + this.healthData.client_perf.read_op_per_sec;
-    const calcPercentage = (status) =>
-      Math.round(((this.healthData.client_perf[status] || 0) / total) * 100);
 
-    ratioLabels.push(`${this.i18n('Writes')} (${calcPercentage('write_op_per_sec')}%)`);
+    ratioLabels.push(
+      `${this.i18n('Writes')} (${this.calcPercentage(
+        this.healthData.client_perf.write_op_per_sec,
+        total
+      )}%)`
+    );
     ratioData.push(this.healthData.client_perf.write_op_per_sec);
-    ratioLabels.push(`${this.i18n('Reads')} (${calcPercentage('read_op_per_sec')}%)`);
+    ratioLabels.push(
+      `${this.i18n('Reads')} (${this.calcPercentage(
+        this.healthData.client_perf.read_op_per_sec,
+        total
+      )}%)`
+    );
     ratioData.push(this.healthData.client_perf.read_op_per_sec);
 
     chart.dataset[0].data = ratioData;
@@ -76,20 +119,17 @@ export class HealthComponent implements OnInit, OnDestroy {
   }
 
   prepareRawUsage(chart, data) {
-    const percentAvailable = Math.round(
-      100 *
-        ((data.df.stats.total_bytes - data.df.stats.total_used_raw_bytes) /
-          data.df.stats.total_bytes)
+    const percentAvailable = this.calcPercentage(
+      data.df.stats.total_bytes - data.df.stats.total_used_raw_bytes,
+      data.df.stats.total_bytes
     );
-
-    const percentUsed = Math.round(
-      100 * (data.df.stats.total_used_raw_bytes / data.df.stats.total_bytes)
+    const percentUsed = this.calcPercentage(
+      data.df.stats.total_used_raw_bytes,
+      data.df.stats.total_bytes
     );
 
     chart.dataset[0].data = [data.df.stats.total_used_raw_bytes, data.df.stats.total_avail_bytes];
-    if (chart === 'doughnut') {
-      chart.options.cutoutPercentage = 65;
-    }
+
     chart.labels = [
       `${this.dimlessBinary.transform(data.df.stats.total_used_raw_bytes)} ${this.i18n(
         'Used'
@@ -99,25 +139,13 @@ export class HealthComponent implements OnInit, OnDestroy {
       )} ${this.i18n('Avail.')} (${percentAvailable}%)`
     ];
 
-    chart.options.title = {
-      display: true,
-      text: `${this.dimlessBinary.transform(data.df.stats.total_bytes)} total`,
-      position: 'bottom'
-    };
+    chart.options.title.text = `${this.dimlessBinary.transform(
+      data.df.stats.total_bytes
+    )} ${this.i18n('total')}`;
   }
 
   preparePgStatus(chart, data) {
     const categoryPgAmount = {};
-    chart.colors = [
-      {
-        backgroundColor: [
-          HealthPieColor.DEFAULT_GREEN,
-          HealthPieColor.DEFAULT_BLUE,
-          HealthPieColor.DEFAULT_ORANGE,
-          HealthPieColor.DEFAULT_RED
-        ]
-      }
-    ];
 
     _.forEach(data.pg_info.statuses, (pgAmount, pgStatesText) => {
       const categoryType = this.pgCategoryService.getTypeByStates(pgStatesText);
@@ -132,15 +160,62 @@ export class HealthComponent implements OnInit, OnDestroy {
       .getAllTypes()
       .map((categoryType) => categoryPgAmount[categoryType]);
 
-    const calcPercentage = (status) =>
-      Math.round(((categoryPgAmount[status] || 0) / data.pg_info.pgs_per_osd) * 100) || 0;
+    chart.labels = [
+      `${this.i18n('Clean')} (${this.calcPercentage(
+        categoryPgAmount['clean'],
+        data.pg_info.pgs_per_osd
+      )}%)`,
+      `${this.i18n('Working')} (${this.calcPercentage(
+        categoryPgAmount['working'],
+        data.pg_info.pgs_per_osd
+      )}%)`,
+      `${this.i18n('Warning')} (${this.calcPercentage(
+        categoryPgAmount['warning'],
+        data.pg_info.pgs_per_osd
+      )}%)`,
+      `${this.i18n('Unknown')} (${this.calcPercentage(
+        categoryPgAmount['unknown'],
+        data.pg_info.pgs_per_osd
+      )}%)`
+    ];
+  }
+
+  prepareObjects(chart, data) {
+    const totalReplicas = data.pg_info.object_stats.num_object_copies;
+    const healthy =
+      totalReplicas -
+      data.pg_info.object_stats.num_objects_misplaced -
+      data.pg_info.object_stats.num_objects_degraded -
+      data.pg_info.object_stats.num_objects_unfound;
 
     chart.labels = [
-      `${this.i18n('Clean')} (${calcPercentage('clean')}%)`,
-      `${this.i18n('Working')} (${calcPercentage('working')}%)`,
-      `${this.i18n('Warning')} (${calcPercentage('warning')}%)`,
-      `${this.i18n('Unknown')} (${calcPercentage('unknown')}%)`
+      `${this.i18n('Healthy')} (${this.calcPercentage(healthy, totalReplicas)}%)`,
+      `${this.i18n('Misplaced')} (${this.calcPercentage(
+        data.pg_info.object_stats.num_objects_misplaced,
+        totalReplicas
+      )}%)`,
+      `${this.i18n('Degraded')} (${this.calcPercentage(
+        data.pg_info.object_stats.num_objects_degraded,
+        totalReplicas
+      )}%)`,
+      `${this.i18n('Unfound')} (${this.calcPercentage(
+        data.pg_info.object_stats.num_objects_unfound,
+        totalReplicas
+      )}%)`
     ];
+
+    chart.dataset[0].data = [
+      healthy,
+      data.pg_info.object_stats.num_objects_misplaced,
+      data.pg_info.object_stats.num_objects_degraded,
+      data.pg_info.object_stats.num_objects_unfound
+    ];
+
+    chart.options.title.text = `${this.dimless.transform(
+      data.pg_info.object_stats.num_objects
+    )} ${this.i18n('total')} (${this.dimless.transform(totalReplicas)} ${this.i18n('replicas')})`;
+
+    chart.options.maintainAspectRatio = window.innerWidth >= 375;
   }
 
   isClientReadWriteChartShowable() {
@@ -149,4 +224,12 @@ export class HealthComponent implements OnInit, OnDestroy {
 
     return readOps + writeOps > 0;
   }
+
+  private calcPercentage(dividend: number, divisor: number) {
+    if (!_.isNumber(dividend) || !_.isNumber(divisor) || divisor === 0) {
+      return 0;
+    }
+
+    return Math.round((dividend / divisor) * 100);
+  }
 }
index a1905afbfd80380e35d2bc3aa1cfaf44c76a3f6a..4e14dd84fb6ff4fdffeeb50e14291d8f4ed55940 100644 (file)
@@ -18,6 +18,11 @@ except ImportError:
 
 from .. import logger, mgr
 
+try:
+    from typing import Dict, Any  # pylint: disable=unused-import
+except ImportError:
+    pass  # For typing only
+
 
 class SendCommandError(rados.Error):
     def __init__(self, err, prefix, argdict, errno):
@@ -40,7 +45,7 @@ class CephService(object):
 
     @classmethod
     def get_service_map(cls, service_name):
-        service_map = {}
+        service_map = {}  # type: Dict[str, Dict[str, Any]]
         for server in mgr.list_servers():
             for service in server['services']:
                 if service['type'] == service_name:
@@ -234,6 +239,9 @@ class CephService(object):
     @classmethod
     def get_pg_info(cls):
         pg_summary = mgr.get('pg_summary')
+        object_stats = {stat: pg_summary['pg_stats_sum']['stat_sum'][stat] for stat in [
+            'num_objects', 'num_object_copies', 'num_objects_degraded',
+            'num_objects_misplaced', 'num_objects_unfound']}
 
         pgs_per_osd = 0.0
         total_osds = len(pg_summary['by_osd'])
@@ -246,6 +254,7 @@ class CephService(object):
             pgs_per_osd = total_pgs / total_osds
 
         return {
+            'object_stats': object_stats,
             'statuses': pg_summary['all'],
             'pgs_per_osd': pgs_per_osd,
         }