]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Landing Page improvements 37390/head
authorAlfonso Martínez <almartin@redhat.com>
Thu, 13 Aug 2020 12:29:38 +0000 (14:29 +0200)
committerTiago Melo <tmelo@suse.com>
Thu, 24 Sep 2020 11:03:29 +0000 (11:03 +0000)
Fixes: https://tracker.ceph.com/issues/42072
Signed-off-by: Alfonso Martínez <almartin@redhat.com>
(cherry picked from commit d66e684b9ec83cca8a58b0a7b8661c568eb0cf6d)

 Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss
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.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.scss
src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss
  this file doesn't exist in octopus, so I moved the code into:
src/pybind/mgr/dashboard/frontend/src/stykes/defaults.scss

14 files changed:
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie-color.enum.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss
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/frontend/src/app/ceph/dashboard/info-card/info-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.scss
src/pybind/mgr/dashboard/frontend/src/styles.scss
src/pybind/mgr/dashboard/frontend/src/styles/bootstrap-extends.scss
src/pybind/mgr/dashboard/frontend/src/styles/defaults.scss

index f149a4b0ab34c0b8ae26b4d6ba5b23f5c84bc9d8..397745f9745d060253df88edad364d43c4021d45 100644 (file)
@@ -45,23 +45,22 @@ describe('Dashboard Main Page', () => {
       // order, checks for card title and position via indexing into a list of all info cards.
       const order = [
         'Cluster Status',
+        'Hosts',
         'Monitors',
         'OSDs',
-        'Manager Daemons',
-        'Hosts',
+        'Managers',
         'Object Gateways',
         'Metadata Servers',
         'iSCSI Gateways',
-        'Client IOPS',
-        'Client Throughput',
-        'Client Read/Write',
-        'Recovery Throughput',
-        'Scrub',
-        'Pools',
         'Raw Capacity',
         'Objects',
+        'PG Status',
+        'Pools',
         'PGs per OSD',
-        'PG Status'
+        'Client Read/Write',
+        'Client Throughput',
+        'Recovery Throughput',
+        'Scrubbing'
       ];
 
       for (let i = 0; i < order.length; i++) {
@@ -72,8 +71,8 @@ describe('Dashboard Main Page', () => {
     it('should verify that info card group titles are present and in the right order', () => {
       cy.location('hash').should('eq', '#/dashboard');
       dashboard.infoGroupTitle(0).should('eq', 'Status');
-      dashboard.infoGroupTitle(1).should('eq', 'Performance');
-      dashboard.infoGroupTitle(2).should('eq', 'Capacity');
+      dashboard.infoGroupTitle(1).should('eq', 'Capacity');
+      dashboard.infoGroupTitle(2).should('eq', 'Performance');
     });
   });
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie-color.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie-color.enum.ts
deleted file mode 100644 (file)
index fbeadcc..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-export enum HealthPieColor {
-  DEFAULT_RED = '#ff7592',
-  DEFAULT_BLUE = '#1d699d',
-  DEFAULT_ORANGE = '#ffa500',
-  DEFAULT_MAGENTA = '#564d65',
-  DEFAULT_GREEN = '#00bb00'
-}
index 02b72b25119dfc7422a730df3cefcb9e15b89e76..ba8176beab3b505aa95e9916011fceb0a22666cc 100644 (file)
@@ -6,6 +6,7 @@
           [options]="chartConfig.options"
           [labels]="chartConfig.labels"
           [colors]="chartConfig.colors"
+          [plugins]="doughnutChartPlugins"
           class="chart-canvas">
   </canvas>
   <div class="chartjs-tooltip"
index e2d23cd4f04079be29796792ef12e2744db460b4..9b7db156bec4e7f75c7625a9252a7aa740587e2d 100644 (file)
@@ -1,9 +1,14 @@
-@import '../../../../styles/chart-tooltip.scss';
+@import './src/styles/chart-tooltip.scss';
+@import './src/styles/defaults';
 
 $canvas-width: 100%;
 $canvas-height: 100%;
 
 .chart-container {
+  @each $key_name, $value in $health-chart-colors {
+    --color-#{$key_name}: #{$value};
+  }
+
   position: unset;
   width: $canvas-width;
   height: $canvas-height;
index 25782502ca55b8d8fdd042a45ddda84c0d2bb29c..96331ff0a5beec2d2ea07fae6ee7a5549d18fab0 100644 (file)
@@ -55,6 +55,16 @@ describe('HealthPieComponent', () => {
     expect(component.chartConfig.dataset[0].data).toEqual(initialData);
   });
 
+  it('should set colors from css variables', () => {
+    const cssVar = '--my-color';
+    const cssVarColor = '#73c5c5';
+    component['getCssVar'] = (name: string) => (name === cssVar ? cssVarColor : '');
+    component.chartConfig.colors[0].backgroundColor = [cssVar, '#ffffff'];
+    fixture.detectChanges();
+
+    expect(component.chartConfig.colors[0].backgroundColor).toEqual([cssVarColor, '#ffffff']);
+  });
+
   describe('tooltip body', () => {
     const tooltipBody = ['text: 10000'];
 
index f00d73f5ae6a0d0a29caa23c3f47e1617d2cd942..0c08973da8a4fae9de950d8cb9a948a20e1abbf3 100644 (file)
@@ -11,11 +11,11 @@ import {
 
 import * as Chart from 'chart.js';
 import * as _ from 'lodash';
+import { PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts';
 
 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({
   selector: 'cd-health-pie',
@@ -42,62 +42,102 @@ export class HealthPieComponent implements OnChanges, OnInit {
   prepareFn = new EventEmitter();
 
   chartConfig: any = {
-    chartType: 'pie',
+    chartType: 'doughnut',
     dataset: [
       {
         label: null,
         borderWidth: 0
       }
     ],
+    colors: [
+      {
+        backgroundColor: [
+          '--color-green',
+          '--color-yellow',
+          '--color-orange',
+          '--color-red',
+          '--color-blue'
+        ]
+      }
+    ],
     options: {
+      cutoutPercentage: 90,
+      events: ['click', 'mouseout', 'touchstart'],
       legend: {
         display: true,
         position: 'right',
-        labels: { usePointStyle: true },
-        onClick: (event: any, legendItem: any) => {
-          this.onLegendClick(event, legendItem);
+        labels: {
+          boxWidth: 10,
+          usePointStyle: false
         }
       },
-      animation: { duration: 0 },
+      plugins: {
+        center_text: true
+      },
       tooltips: {
-        enabled: false
+        enabled: true,
+        displayColors: false,
+        backgroundColor: 'rgba(0,0,0,0.8)',
+        cornerRadius: 0,
+        bodyFontSize: 14,
+        bodyFontStyle: '600',
+        position: 'nearest',
+        xPadding: 12,
+        yPadding: 12,
+        callbacks: {
+          label: (item: Record<string, any>, data: Record<string, any>) => {
+            let text = data.labels[item.index];
+            if (!text.includes('%')) {
+              text = `${text} (${data.datasets[item.datasetIndex].data[item.index]}%)`;
+            }
+            return text;
+          }
+        }
       },
       title: {
         display: false
       }
     }
   };
-  private hiddenSlices: any[] = [];
-
-  constructor(private dimlessBinary: DimlessBinaryPipe, private dimless: DimlessPipe) {}
 
-  ngOnInit() {
-    // An extension to Chart.js to enable rendering some
-    // text in the middle of a doughnut
-    Chart.pluginService.register({
-      beforeDraw: function (chart: any) {
-        if (!chart.options.center_text) {
+  public doughnutChartPlugins: PluginServiceGlobalRegistrationAndOptions[] = [
+    {
+      id: 'center_text',
+      beforeDraw(chart: Chart) {
+        const defaultFontColorA = '#151515';
+        const defaultFontColorB = '#72767B';
+        const defaultFontFamily = 'Helvetica Neue, Helvetica, Arial, sans-serif';
+        Chart.defaults.global.defaultFontFamily = defaultFontFamily;
+        const ctx = chart.ctx;
+        if (!chart.options.plugins.center_text || !chart.data.datasets[0].label) {
           return;
         }
 
-        const width = chart.chart.width,
-          height = chart.chart.height,
-          ctx = chart.chart.ctx;
+        ctx.save();
+        const label = chart.data.datasets[0].label.split('\n');
 
-        ctx.restore();
-        const fontSize = (height / 114).toFixed(2);
-        ctx.font = fontSize + 'em sans-serif';
+        const centerX = (chart.chartArea.left + chart.chartArea.right) / 2;
+        const centerY = (chart.chartArea.top + chart.chartArea.bottom) / 2;
+        ctx.textAlign = 'center';
         ctx.textBaseline = 'middle';
 
-        const text = chart.options.center_text,
-          textX = Math.round((width - ctx.measureText(text).width) / 2),
-          textY = height / 2;
+        ctx.font = `24px ${defaultFontFamily}`;
+        ctx.fillStyle = defaultFontColorA;
+        ctx.fillText(label[0], centerX, centerY - 10);
 
-        ctx.fillText(text, textX, textY);
-        ctx.save();
+        if (label.length > 1) {
+          ctx.font = `14px ${defaultFontFamily}`;
+          ctx.fillStyle = defaultFontColorB;
+          ctx.fillText(label[1], centerX, centerY + 10);
+        }
+        ctx.restore();
       }
-    });
+    }
+  ];
+
+  constructor(private dimlessBinary: DimlessBinaryPipe, private dimless: DimlessPipe) {}
 
+  ngOnInit() {
     const getStyleTop = (tooltip: any, positionY: number) => {
       return positionY + tooltip.caretY - tooltip.height - 10 + 'px';
     };
@@ -113,39 +153,40 @@ export class HealthPieComponent implements OnChanges, OnInit {
       getStyleTop
     );
 
-    const getBody = (body: any) => {
+    chartTooltip.getBody = (body: any) => {
       return this.getChartTooltipBody(body);
     };
 
-    chartTooltip.getBody = getBody;
-
-    this.chartConfig.options.tooltips.custom = (tooltip: any) => {
-      chartTooltip.customTooltips(tooltip);
-    };
-
-    this.chartConfig.colors = [
-      {
-        backgroundColor: [
-          HealthPieColor.DEFAULT_RED,
-          HealthPieColor.DEFAULT_BLUE,
-          HealthPieColor.DEFAULT_ORANGE,
-          HealthPieColor.DEFAULT_GREEN,
-          HealthPieColor.DEFAULT_MAGENTA
-        ]
-      }
-    ];
-
     _.merge(this.chartConfig, this.config);
 
+    this.setColorsFromCssVars();
+
     this.prepareFn.emit([this.chartConfig, this.data]);
   }
 
   ngOnChanges() {
     this.prepareFn.emit([this.chartConfig, this.data]);
-    this.hideSlices();
     this.setChartSliceBorderWidth();
   }
 
+  private setColorsFromCssVars() {
+    this.chartConfig.colors.forEach(
+      (colorEl: { backgroundColor: string[] }, colorIndex: number) => {
+        colorEl.backgroundColor.forEach((bgColor: string, bgColorIndex: number) => {
+          if (bgColor.startsWith('--')) {
+            this.chartConfig.colors[colorIndex].backgroundColor[bgColorIndex] = this.getCssVar(
+              bgColor
+            );
+          }
+        });
+      }
+    );
+  }
+
+  private getCssVar(name: string): string {
+    return getComputedStyle(document.querySelector('.chart-container')).getPropertyValue(name);
+  }
+
   private getChartTooltipBody(body: string[]) {
     const bodySplit = body[0].split(': ');
 
@@ -170,18 +211,4 @@ export class HealthPieComponent implements OnChanges, OnInit {
 
     this.chartConfig.dataset[0].borderWidth = nonZeroValueSlices > 1 ? 1 : 0;
   }
-
-  private onLegendClick(event: any, legendItem: any) {
-    event.stopPropagation();
-    this.hiddenSlices[legendItem.index] = !legendItem.hidden;
-    this.ngOnChanges();
-  }
-
-  private hideSlices() {
-    _.forEach(this.chartConfig.dataset[0].data, (_slice, sliceIndex: number) => {
-      if (this.hiddenSlices[sliceIndex]) {
-        this.chartConfig.dataset[0].data[sliceIndex] = undefined;
-      }
-    });
-  }
 }
index 84ce11fb23cbdf1651f6e313d47cd119ff5508ed..409bf08eaecbffe9e91cdf1ce2550d978b3e5cf8 100644 (file)
@@ -34,7 +34,8 @@
              container="body"
              containerClass="info-card-popover-cluster-status"
              (click)="healthChecksTarget.toggle()">
-          {{ healthData.health.status }}
+          {{ healthData.health.status }} <i *ngIf="healthData.health?.status != 'HEALTH_OK'"
+                                            class="fa fa-exclamation-triangle"></i>
         </div>
       </ng-container>
       <ng-container *ngIf="!healthData.health?.checks?.length">
       </ng-container>
     </cd-info-card>
 
+    <cd-info-card cardTitle="Hosts"
+                  i18n-cardTitle
+                  link="/hosts"
+                  class="cd-status-card"
+                  contentClass="content-highlight"
+                  *ngIf="healthData.hosts != null">
+      {{ healthData.hosts }} total
+    </cd-info-card>
+
     <cd-info-card cardTitle="Monitors"
                   i18n-cardTitle
                   link="/monitor"
@@ -65,7 +75,7 @@
       </span>
     </cd-info-card>
 
-    <cd-info-card cardTitle="Manager Daemons"
+    <cd-info-card cardTitle="Managers"
                   i18n-cardTitle
                   class="cd-status-card"
                   contentClass="content-highlight"
       </span>
     </cd-info-card>
 
-    <cd-info-card cardTitle="Hosts"
-                  i18n-cardTitle
-                  link="/hosts"
-                  class="cd-status-card"
-                  contentClass="content-highlight"
-                  *ngIf="healthData.hosts != null">
-      {{ healthData.hosts }} total
-    </cd-info-card>
-
     <cd-info-card cardTitle="Object Gateways"
                   i18n-cardTitle
                   link="/rgw/daemon"
@@ -98,7 +99,7 @@
     <cd-info-card cardTitle="Metadata Servers"
                   i18n-cardTitle
                   class="cd-status-card"
-                  *ngIf="((enabledFeature.cephfs && healthData.fs_map) | mdsSummary) as transformedResult"
+                  *ngIf="(enabledFeature.cephfs && healthData.fs_map | mdsSummary) as transformedResult"
                   [contentClass]="(transformedResult.length > 1 ? 'text-area-size-2' : '') + ' content-highlight'">
       <!-- TODO: check text-area-size-2 -->
       <span *ngFor="let result of transformedResult"
     </cd-info-card>
   </cd-info-group>
 
-  <cd-info-group groupTitle="Performance"
-                 i18n-groupTitle
-                 *ngIf="healthData.client_perf || healthData.scrub_status">
-
-    <cd-info-card cardTitle="Client IOPS"
-                  i18n-cardTitle
-                  class="cd-performance-card"
-                  contentClass="content-highlight"
-                  *ngIf="healthData.client_perf">
-      {{ (healthData.client_perf.read_op_per_sec + healthData.client_perf.write_op_per_sec) | round:1 }}
-    </cd-info-card>
-
-    <cd-info-card cardTitle="Client Throughput"
-                  i18n-cardTitle
-                  class="cd-performance-card"
-                  contentClass="content-highlight"
-                  *ngIf="healthData.client_perf">
-      {{ ((healthData.client_perf.read_bytes_sec + healthData.client_perf.write_bytes_sec) | dimlessBinary) + '/s' }}
-    </cd-info-card>
-
-    <cd-info-card cardTitle="Client Read/Write"
-                  i18n-cardTitle
-                  class="cd-performance-card"
-                  [contentClass]="isClientReadWriteChartShowable() ? 'content-chart': 'content-highlight'"
-                  *ngIf="healthData.client_perf">
-      <cd-health-pie *ngIf="isClientReadWriteChartShowable()"
-                     [data]="healthData"
-                     (prepareFn)="prepareReadWriteRatio($event[0], $event[1])">
-      </cd-health-pie>
-      <span *ngIf="!isClientReadWriteChartShowable()">
-        N/A
-      </span>
-    </cd-info-card>
-
-    <cd-info-card cardTitle="Recovery Throughput"
-                  i18n-cardTitle
-                  class="cd-performance-card"
-                  contentClass="content-highlight"
-                  *ngIf="healthData.client_perf">
-      {{ (healthData.client_perf.recovering_bytes_per_sec | dimlessBinary) + '/s' }}
-    </cd-info-card>
-
-    <cd-info-card cardTitle="Scrub"
-                  i18n-cardTitle
-                  class="cd-performance-card"
-                  contentClass="content-highlight"
-                  *ngIf="healthData.scrub_status">
-      {{ healthData.scrub_status }}
-    </cd-info-card>
-  </cd-info-group>
-
   <cd-info-group groupTitle="Capacity"
                  i18n-groupTitle
                  *ngIf="healthData.pools
                  || healthData.df
                  || healthData.pg_info">
-    <cd-info-card cardTitle="Pools"
-                  i18n-cardTitle
-                  link="/pool"
-                  class="cd-capacity-card order-md-1 order-lg-4 order-xl-1"
-                  contentClass="content-highlight"
-                  *ngIf="healthData.pools">
-      {{ healthData.pools.length }}
-    </cd-info-card>
-
     <cd-info-card cardTitle="Raw Capacity"
                   i18n-cardTitle
-                  class="cd-capacity-card order-md-3 order-lg-1 order-xl-2"
+                  class="cd-capacity-card cd-chart-card"
                   contentClass="content-chart"
                   *ngIf="healthData.df">
       <cd-health-pie [data]="healthData"
 
     <cd-info-card cardTitle="Objects"
                   i18n-cardTitle
-                  class="cd-capacity-card order-md-4 order-lg-2 order-xl-3"
+                  class="cd-capacity-card cd-chart-card"
                   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"
-                  i18n-cardTitle
-                  class="cd-capacity-card order-md-2 order-lg-5 order-xl-4"
-                  contentClass="content-highlight"
-                  *ngIf="healthData.pg_info">
-      {{ healthData.pg_info.pgs_per_osd | dimless }}
-    </cd-info-card>
-
     <cd-info-card cardTitle="PG Status"
                   i18n-cardTitle
-                  class="cd-capacity-card order-md-5 order-lg-3 order-xl-5"
+                  class="cd-capacity-card cd-chart-card"
                   contentClass="content-chart"
                   (click)="pgStatusTarget.toggle()"
                   *ngIf="healthData.pg_info">
         </div>
       </div>
     </cd-info-card>
+
+    <cd-info-card cardTitle="Pools"
+                  i18n-cardTitle
+                  link="/pool"
+                  class="cd-capacity-card"
+                  contentClass="content-highlight"
+                  *ngIf="healthData.pools">
+      {{ healthData.pools.length }}
+    </cd-info-card>
+
+    <cd-info-card cardTitle="PGs per OSD"
+                  i18n-cardTitle
+                  class="cd-capacity-card"
+                  contentClass="content-highlight"
+                  *ngIf="healthData.pg_info">
+      {{ healthData.pg_info.pgs_per_osd | dimless }}
+    </cd-info-card>
+  </cd-info-group>
+
+  <cd-info-group groupTitle="Performance"
+                 i18n-groupTitle
+                 *ngIf="healthData.client_perf || healthData.scrub_status">
+    <cd-info-card cardTitle="Client Read/Write"
+                  i18n-cardTitle
+                  class="cd-performance-card cd-chart-card"
+                  contentClass="content-chart"
+                  *ngIf="healthData.client_perf">
+      <cd-health-pie [data]="healthData"
+                     [config]="clientStatsConfig"
+                     (prepareFn)="prepareReadWriteRatio($event[0], $event[1])">
+      </cd-health-pie>
+    </cd-info-card>
+
+    <cd-info-card cardTitle="Client Throughput"
+                  i18n-cardTitle
+                  class="cd-performance-card cd-chart-card"
+                  contentClass="content-chart"
+                  *ngIf="healthData.client_perf">
+      <cd-health-pie [data]="healthData"
+                     [config]="clientStatsConfig"
+                     (prepareFn)="prepareClientThroughput($event[0], $event[1])">
+      </cd-health-pie>
+    </cd-info-card>
+
+    <cd-info-card cardTitle="Recovery Throughput"
+                  i18n-cardTitle
+                  class="cd-performance-card"
+                  contentClass="content-highlight"
+                  *ngIf="healthData.client_perf">
+      {{ (healthData.client_perf.recovering_bytes_per_sec | dimlessBinary) + '/s' }}
+    </cd-info-card>
+
+    <cd-info-card cardTitle="Scrubbing"
+                  i18n-cardTitle
+                  class="cd-performance-card"
+                  contentClass="content-highlight"
+                  *ngIf="healthData.scrub_status">
+      {{ healthData.scrub_status }}
+    </cd-info-card>
   </cd-info-group>
 
   <ng-template #logsLink>
index 50a96ee6b51b58da2779cdbbf0a7020cc9eda28f..d2fea1406cdcd6aecd96e8adfc61eaf8899e4471 100644 (file)
@@ -94,7 +94,7 @@ describe('HealthComponent', () => {
     expect(infoGroups.length).toBe(3);
 
     const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
-    expect(infoCards.length).toBe(18);
+    expect(infoCards.length).toBe(17);
   });
 
   describe('features disabled', () => {
@@ -119,7 +119,7 @@ describe('HealthComponent', () => {
       expect(infoGroups.length).toBe(3);
 
       const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
-      expect(infoCards.length).toBe(15);
+      expect(infoCards.length).toBe(14);
     });
   });
 
@@ -141,7 +141,7 @@ describe('HealthComponent', () => {
     expect(infoGroups.length).toBe(2);
 
     const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
-    expect(infoCards.length).toBe(10);
+    expect(infoCards.length).toBe(9);
   });
 
   it('should render all except "Performance" group and cards', () => {
@@ -172,7 +172,7 @@ describe('HealthComponent', () => {
     expect(infoGroups.length).toBe(2);
 
     const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
-    expect(infoCards.length).toBe(13);
+    expect(infoCards.length).toBe(12);
   });
 
   it('should render all groups and 1 card per group', () => {
@@ -256,17 +256,25 @@ describe('HealthComponent', () => {
   });
 
   describe('preparePgStatus', () => {
-    const calcPercentage = (data: number) => Math.round((data / 10) * 100) || 0;
-
-    const expectedChart = (data: number[]) => ({
+    const expectedChart = (data: number[], label: string = null) => ({
       labels: [
-        `Clean (${calcPercentage(data[0])}%)`,
-        `Working (${calcPercentage(data[1])}%)`,
-        `Warning (${calcPercentage(data[2])}%)`,
-        `Unknown (${calcPercentage(data[3])}%)`
+        `Clean: ${component['dimless'].transform(data[0])}`,
+        `Working: ${component['dimless'].transform(data[1])}`,
+        `Warning: ${component['dimless'].transform(data[2])}`,
+        `Unknown: ${component['dimless'].transform(data[3])}`
       ],
       options: {},
-      dataset: [{ data: data }]
+      dataset: [
+        {
+          data: data.map((i) =>
+            component['calcPercentage'](
+              i,
+              data.reduce((j, k) => j + k)
+            )
+          ),
+          label: label
+        }
+      ]
     });
 
     it('gets no data', () => {
@@ -274,7 +282,7 @@ describe('HealthComponent', () => {
       component.preparePgStatus(chart, {
         pg_info: {}
       });
-      expect(chart).toEqual(expectedChart([undefined, undefined, undefined, undefined]));
+      expect(chart).toEqual(expectedChart([0, 0, 0, 0], '0\nPGs'));
     });
 
     it('gets data from all categories', () => {
@@ -289,7 +297,7 @@ describe('HealthComponent', () => {
           }
         }
       });
-      expect(chart).toEqual(expectedChart([1, 2, 3, 4]));
+      expect(chart).toEqual(expectedChart([1, 2, 3, 4], '10\nPGs'));
     });
   });
 
index 7b6e5f8a0fbc92210348902c78f9f75bc20be9f5..11c048374dedb527bdc303f58b3f9eaa51a0a426 100644 (file)
@@ -16,7 +16,6 @@ import {
 } from '../../../shared/services/feature-toggles.service';
 import { RefreshIntervalService } from '../../../shared/services/refresh-interval.service';
 import { PgCategoryService } from '../../shared/pg-category.service';
-import { HealthPieColor } from '../health-pie/health-pie-color.enum';
 
 @Component({
   selector: 'cd-health',
@@ -30,39 +29,28 @@ export class HealthComponent implements OnInit, OnDestroy {
   enabledFeature$: FeatureTogglesMap$;
   icons = Icons;
 
-  rawCapacityChartConfig = {
-    options: {
-      title: { display: true, position: 'bottom' }
-    }
-  };
-  objectsChartConfig = {
-    options: {
-      title: { display: true, position: 'bottom' }
-    },
+  clientStatsConfig = {
     colors: [
       {
-        backgroundColor: [
-          HealthPieColor.DEFAULT_GREEN,
-          HealthPieColor.DEFAULT_MAGENTA,
-          HealthPieColor.DEFAULT_ORANGE,
-          HealthPieColor.DEFAULT_RED
-        ]
+        backgroundColor: ['--color-cyan', '--color-purple']
       }
     ]
   };
-  pgStatusChartConfig = {
+
+  rawCapacityChartConfig = {
     colors: [
       {
-        backgroundColor: [
-          HealthPieColor.DEFAULT_GREEN,
-          HealthPieColor.DEFAULT_BLUE,
-          HealthPieColor.DEFAULT_ORANGE,
-          HealthPieColor.DEFAULT_RED
-        ]
+        backgroundColor: ['--color-blue', '--color-gray']
       }
     ]
   };
 
+  pgStatusChartConfig = {
+    options: {
+      events: ['']
+    }
+  };
+
   constructor(
     private healthService: HealthService,
     private i18n: I18n,
@@ -102,22 +90,44 @@ export class HealthComponent implements OnInit, OnDestroy {
       this.healthData.client_perf.write_op_per_sec + this.healthData.client_perf.read_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')} (${this.calcPercentage(
-        this.healthData.client_perf.read_op_per_sec,
-        total
-      )}%)`
+      `${this.i18n(`Reads`)}: ${this.dimless.transform(
+        this.healthData.client_perf.read_op_per_sec
+      )} ${this.i18n(`/s`)}`
     );
     ratioData.push(this.healthData.client_perf.read_op_per_sec);
+    ratioLabels.push(this.healthData.client_perf.write_op_per_sec);
+    ratioData.push(this.calcPercentage(this.healthData.client_perf.write_op_per_sec, total));
 
+    chart.labels = ratioLabels;
     chart.dataset[0].data = ratioData;
+    chart.dataset[0].label = `${this.dimless.transform(total)}\n${this.i18n(`IOPS`)}`;
+  }
+
+  prepareClientThroughput(chart: Record<string, any>) {
+    const ratioLabels = [];
+    const ratioData = [];
+
+    const total =
+      this.healthData.client_perf.read_bytes_sec + this.healthData.client_perf.write_bytes_sec;
+
+    ratioLabels.push(
+      `${this.i18n(`Reads`)}: ${this.dimlessBinary.transform(
+        this.healthData.client_perf.read_bytes_sec
+      )}${this.i18n(`/s`)}`
+    );
+    ratioData.push(this.calcPercentage(this.healthData.client_perf.read_bytes_sec, total));
+    ratioLabels.push(
+      `${this.i18n(`Writes`)}: ${this.dimlessBinary.transform(
+        this.healthData.client_perf.write_bytes_sec
+      )}${this.i18n(`/s`)}`
+    );
+    ratioData.push(this.calcPercentage(this.healthData.client_perf.write_bytes_sec, total));
+
     chart.labels = ratioLabels;
+    chart.dataset[0].data = ratioData;
+    chart.dataset[0].label = `${this.dimlessBinary.transform(total).replace(' ', '\n')}${this.i18n(
+      `/s`
+    )}`;
   }
 
   prepareRawUsage(chart: Record<string, any>, data: Record<string, any>) {
@@ -130,20 +140,18 @@ export class HealthComponent implements OnInit, OnDestroy {
       data.df.stats.total_bytes
     );
 
-    chart.dataset[0].data = [data.df.stats.total_used_raw_bytes, data.df.stats.total_avail_bytes];
+    chart.dataset[0].data = [percentUsed, percentAvailable];
 
     chart.labels = [
-      `${this.dimlessBinary.transform(data.df.stats.total_used_raw_bytes)} ${this.i18n(
-        'Used'
-      )} (${percentUsed}%)`,
-      `${this.dimlessBinary.transform(
+      `${this.i18n(`Used`)}: ${this.dimlessBinary.transform(data.df.stats.total_used_raw_bytes)}`,
+      `${this.i18n(`Avail.`)}: ${this.dimlessBinary.transform(
         data.df.stats.total_bytes - data.df.stats.total_used_raw_bytes
-      )} ${this.i18n('Avail.')} (${percentAvailable}%)`
+      )}`
     ];
 
-    chart.options.title.text = `${this.dimlessBinary.transform(
+    chart.dataset[0].label = `${percentUsed}%\nof ${this.dimlessBinary.transform(
       data.df.stats.total_bytes
-    )} ${this.i18n('total')}`;
+    )}`;
   }
 
   preparePgStatus(chart: Record<string, any>, data: Record<string, any>) {
@@ -160,54 +168,64 @@ export class HealthComponent implements OnInit, OnDestroy {
       totalPgs += pgAmount;
     });
 
+    for (const categoryType of this.pgCategoryService.getAllTypes()) {
+      if (_.isUndefined(categoryPgAmount[categoryType])) {
+        categoryPgAmount[categoryType] = 0;
+      }
+    }
+
     chart.dataset[0].data = this.pgCategoryService
       .getAllTypes()
-      .map((categoryType) => categoryPgAmount[categoryType]);
+      .map((categoryType) => this.calcPercentage(categoryPgAmount[categoryType], totalPgs));
 
     chart.labels = [
-      `${this.i18n('Clean')} (${this.calcPercentage(categoryPgAmount['clean'], totalPgs)}%)`,
-      `${this.i18n('Working')} (${this.calcPercentage(categoryPgAmount['working'], totalPgs)}%)`,
-      `${this.i18n('Warning')} (${this.calcPercentage(categoryPgAmount['warning'], totalPgs)}%)`,
-      `${this.i18n('Unknown')} (${this.calcPercentage(categoryPgAmount['unknown'], totalPgs)}%)`
+      `${this.i18n(`Clean`)}: ${this.dimless.transform(categoryPgAmount['clean'])}`,
+      `${this.i18n(`Working`)}: ${this.dimless.transform(categoryPgAmount['working'])}`,
+      `${this.i18n(`Warning`)}: ${this.dimless.transform(categoryPgAmount['warning'])}`,
+      `${this.i18n(`Unknown`)}: ${this.dimless.transform(categoryPgAmount['unknown'])}`
     ];
+
+    chart.dataset[0].label = `${totalPgs}\n${this.i18n(`PGs`)}`;
   }
 
   prepareObjects(chart: Record<string, any>, data: Record<string, any>) {
-    const totalReplicas = data.pg_info.object_stats.num_object_copies;
+    const objectCopies = data.pg_info.object_stats.num_object_copies;
     const healthy =
-      totalReplicas -
+      objectCopies -
       data.pg_info.object_stats.num_objects_misplaced -
       data.pg_info.object_stats.num_objects_degraded -
       data.pg_info.object_stats.num_objects_unfound;
+    const healthyPercentage = this.calcPercentage(healthy, objectCopies);
+    const misplacedPercentage = this.calcPercentage(
+      data.pg_info.object_stats.num_objects_misplaced,
+      objectCopies
+    );
+    const degradedPercentage = this.calcPercentage(
+      data.pg_info.object_stats.num_objects_degraded,
+      objectCopies
+    );
+    const unfoundPercentage = this.calcPercentage(
+      data.pg_info.object_stats.num_objects_unfound,
+      objectCopies
+    );
 
     chart.labels = [
-      `${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
-      )}%)`
+      `${this.i18n(`Healthy`)}: ${healthyPercentage}%`,
+      `${this.i18n(`Misplaced`)}: ${misplacedPercentage}%`,
+      `${this.i18n(`Degraded`)}: ${degradedPercentage}%`,
+      `${this.i18n(`Unfound`)}: ${unfoundPercentage}%`
     ];
 
     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
+      healthyPercentage,
+      misplacedPercentage,
+      degradedPercentage,
+      unfoundPercentage
     ];
 
-    chart.options.title.text = `${this.dimless.transform(
+    chart.dataset[0].label = `${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;
+    )}\n${this.i18n(`objects`)}`;
   }
 
   isClientReadWriteChartShowable() {
index 526b3e9d74e961d05cfcd446962c3e6c9da6156b..84acc3492bcda347bc59f8ed03de7088ad0a0a83 100644 (file)
@@ -1,14 +1,14 @@
 <div class="card"
      [ngClass]="cardClass">
   <div class="card-body d-flex align-items-center justify-content-center">
-    <h5 class="card-title m-4">
+    <h4 class="card-title m-4">
       <a *ngIf="link; else noLinkTitle"
          [routerLink]="link">{{ cardTitle }}</a>
 
       <ng-template #noLinkTitle>
         {{ cardTitle }}
       </ng-template>
-    </h5>
+    </h4>
 
     <div class="card-text text-center"
          [ngClass]="contentClass">
index 07732f3edc87117eb48edeed5a2ed403b92686f3..22cc7c7dcd30ab2eb2d2fe655a79a4cbe4a3fd2b 100644 (file)
@@ -21,9 +21,9 @@ $card-font-max-size: 21px;
     padding-top: 40px !important;
 
     .card-title {
+      left: -0.6rem;
       position: absolute;
-      left: 0;
-      top: 0;
+      top: -0.3rem;
     }
   }
 }
index 12aec546555c8e11f8cb0326587061d367ed5011..096533658708734961c03e0e070d8832f307a1af 100644 (file)
@@ -22,6 +22,16 @@ $badge-font-size: 1rem;
 $form-feedback-font-size: 100%;
 $popover-max-width: 350px;
 
+// https://getbootstrap.com/docs/4.5/layout/grid/#variables
+$grid-breakpoints: (
+  xs: 0,
+  sm: 576px,
+  md: 768px,
+  lg: 992px,
+  xl: 1200px,
+  2xl: 1450px
+);
+
 @import '~bootstrap/scss/bootstrap';
 @import '~fork-awesome/scss/fork-awesome';
 @import 'app/ceph/dashboard/info-card/info-card-popover.scss';
index 83ea59c76009bda8a4c141fe79954ac9ec6c0954..5932596f82eb1f1629d721b6dc98bf884a0500d6 100644 (file)
@@ -9,6 +9,10 @@ cd-info-card {
     @extend .pb-2;
 
     .card-body {
+      .card-title {
+        @extend .pl-2;
+      }
+
       .card-text {
         @extend .pt-2;
       }
@@ -61,6 +65,21 @@ cd-health {
     &.cd-capacity-card {
       @extend .col-xl;
     }
+
+    &.cd-capacity-card {
+      @extend .col-lg-3;
+    }
+
+    &.cd-performance-card {
+      @extend .col-lg-6;
+    }
+
+    &.cd-chart-card {
+      @extend .col-md-12;
+      @extend .col-lg-6;
+      @extend .col-xl-4;
+      @extend .col-2xl-3;
+    }
   }
 }
 
index 1b4382ca1a0506d4f4d924e585b978c1a6b9eb46..cbae2bb1fd5afb724a725aa06f3280482e6fb7ed 100644 (file)
@@ -192,3 +192,18 @@ $color-rgw-icon: $color-blue-gray !default;
     }
   }
 }
+
+// This was backported from _bootstrap-defaults.scss in master
+$health-chart-colors: (
+  'red': #c9190b,
+  'blue': #06c,
+  'orange': #ef9234,
+  'yellow': #f6d173,
+  'magenta': #009596,
+  'green': #7cc674,
+  'gray': #ededed,
+  'light-blue': #519de9,
+  'light-yellow': #f9e0a2,
+  'cyan': #73c5c5,
+  'purple': #3c3d99
+);