]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: fix a bug where data would plot on the graphs without converting to...
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Thu, 16 Mar 2023 12:09:26 +0000 (13:09 +0100)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Thu, 6 Jul 2023 06:53:34 +0000 (08:53 +0200)
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
(cherry picked from commit 7aa70a16a54c3387b2e021763ca1d118d6785835)

src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary-per-second.pipe.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.ts [new file with mode: 0644]

index 3da4334210de03d56f9b97dc369fb3b395e1690d..0a4f2ae1e966737fcf9f256eaddfcdb2d4ec1cfa 100644 (file)
@@ -1,4 +1,4 @@
-import { AfterViewInit, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
+import { AfterViewInit, Component, Input, OnChanges, ViewChild } from '@angular/core';
 
 import { CssHelper } from '~/app/shared/classes/css-helper';
 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
@@ -6,35 +6,38 @@ import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-pe
 import { FormatterService } from '~/app/shared/services/formatter.service';
 import { BaseChartDirective, PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts';
 import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { NumberFormatterService } from '~/app/shared/services/number-formatter.service';
 
 @Component({
   selector: 'cd-dashboard-area-chart',
   templateUrl: './dashboard-area-chart.component.html',
   styleUrls: ['./dashboard-area-chart.component.scss']
 })
-export class DashboardAreaChartComponent implements OnInit, OnChanges, AfterViewInit {
+export class DashboardAreaChartComponent implements OnChanges, AfterViewInit {
   @ViewChild(BaseChartDirective) chart: BaseChartDirective;
 
   @Input()
   chartTitle: string;
   @Input()
-  maxValue?: any;
+  maxValue?: number;
   @Input()
   dataUnits: string;
   @Input()
-  data: any;
+  data: Array<[number, string]>;
   @Input()
-  data2?: any;
+  data2?: Array<[number, string]>;
   @Input()
-  label: any;
+  label: string;
   @Input()
-  label2?: any;
+  label2?: string;
 
   currentDataUnits: string;
   currentData: number;
   currentDataUnits2?: string;
   currentData2?: number;
 
+  chartDataUnits: string;
+
   chartData: any = {
     dataset: [
       {
@@ -146,21 +149,19 @@ export class DashboardAreaChartComponent implements OnInit, OnChanges, AfterView
     private dimlessBinary: DimlessBinaryPipe,
     private dimlessBinaryPerSecond: DimlessBinaryPerSecondPipe,
     private dimlessPipe: DimlessPipe,
-    private formatter: FormatterService
+    private formatter: FormatterService,
+    private numberFormatter: NumberFormatterService
   ) {}
 
-  ngOnInit(): void {
-    this.currentData = Number(
-      this.chartData.dataset[0].data[this.chartData.dataset[0].data.length - 1].y
-    );
-    if (this.data2) {
-      this.currentData2 = Number(
-        this.chartData.dataset[1].data[this.chartData.dataset[1].data.length - 1].y
-      );
-    }
+  ngOnChanges(): void {
+    this.updateChartData();
   }
 
-  ngOnChanges(): void {
+  ngAfterViewInit(): void {
+    this.updateChartData();
+  }
+
+  private updateChartData(): void {
     if (this.data) {
       this.setChartTicks();
       this.chartData.dataset[0].data = this.formatData(this.data);
@@ -176,11 +177,8 @@ export class DashboardAreaChartComponent implements OnInit, OnChanges, AfterView
         this.data2[this.data2.length - 1][1]
       ).split(' ');
     }
-  }
-
-  ngAfterViewInit(): void {
-    if (this.data) {
-      this.setChartTicks();
+    if (this.chart) {
+      this.chart.chart.update();
     }
   }
 
@@ -188,16 +186,48 @@ export class DashboardAreaChartComponent implements OnInit, OnChanges, AfterView
     let formattedData = {};
     formattedData = array.map((data: any) => ({
       x: data[0] * 1000,
-      y: Number(this.convertUnits(data[1]).replace(/[^\d,.]+/g, ''))
+      y: Number(this.convertToChartDataUnits(data[1]).replace(/[^\d,.]+/g, ''))
     }));
     return formattedData;
   }
 
+  private convertToChartDataUnits(data: any): any {
+    let dataWithUnits: string = '';
+    if (this.chartDataUnits) {
+      if (this.dataUnits === 'B') {
+        dataWithUnits = this.numberFormatter.formatBytesFromTo(
+          data,
+          this.dataUnits,
+          this.chartDataUnits
+        );
+      } else if (this.dataUnits === 'B/s') {
+        dataWithUnits = this.numberFormatter.formatBytesPerSecondFromTo(
+          data,
+          this.dataUnits,
+          this.chartDataUnits
+        );
+      } else if (this.dataUnits === 'ms') {
+        dataWithUnits = this.numberFormatter.formatSecondsFromTo(
+          data,
+          this.dataUnits,
+          this.chartDataUnits
+        );
+      } else {
+        dataWithUnits = this.numberFormatter.formatUnitlessFromTo(
+          data,
+          this.dataUnits,
+          this.chartDataUnits
+        );
+      }
+    }
+    return dataWithUnits;
+  }
+
   private convertUnits(data: any): any {
-    let dataWithUnits: string;
-    if (this.dataUnits === 'bytes') {
+    let dataWithUnits: string = '';
+    if (this.dataUnits === 'B') {
       dataWithUnits = this.dimlessBinary.transform(data);
-    } else if (this.dataUnits === 'bytesPerSecond') {
+    } else if (this.dataUnits === 'B/s') {
       dataWithUnits = this.dimlessBinaryPerSecond.transform(data);
     } else if (this.dataUnits === 'ms') {
       dataWithUnits = this.formatter.format_number(data, 1000, ['ms', 's']);
@@ -220,46 +250,43 @@ export class DashboardAreaChartComponent implements OnInit, OnChanges, AfterView
   }
 
   private setChartTicks() {
-    if (this.chart && this.maxValue) {
-      let [maxValue, maxValueDataUnits] = this.convertUnits(this.maxValue).split(' ');
-      this.chart.chart.options.scales.yAxes[0].ticks.suggestedMax = maxValue;
-      this.chart.chart.options.scales.yAxes[0].ticks.suggestedMin = 0;
-      this.chart.chart.options.scales.yAxes[0].ticks.stepSize = Number((maxValue / 2).toFixed(0));
-      this.chart.chart.options.scales.yAxes[0].ticks.callback = (value: any) => {
-        if (value === 0) {
-          return null;
-        }
-        return this.fillString(`${value} ${maxValueDataUnits}`);
-      };
-      this.chart.chart.update();
-    } else if (this.chart && this.data) {
-      let maxValue = 0,
-        maxValueDataUnits = '';
+    if (!this.chart) {
+      return;
+    }
+
+    let maxValue = 0;
+    let maxValueDataUnits = '';
+    let extraRoom = 1.2;
+
+    if (this.maxValue) {
+      extraRoom = 1.0;
+      [maxValue, maxValueDataUnits] = this.convertUnits(this.maxValue).split(' ');
+    } else if (this.data) {
+      extraRoom = 1.2;
       let maxValueData = Math.max(...this.data.map((values: any) => values[1]));
       if (this.data2) {
-        var maxValueData2 = Math.max(...this.data2.map((values: any) => values[1]));
-        [maxValue, maxValueDataUnits] = this.convertUnits(
-          Math.max(maxValueData, maxValueData2)
-        ).split(' ');
+        let maxValueData2 = Math.max(...this.data2.map((values: any) => values[1]));
+        maxValue = Math.max(maxValueData, maxValueData2);
       } else {
-        [maxValue, maxValueDataUnits] = this.convertUnits(Math.max(maxValueData)).split(' ');
+        maxValue = maxValueData;
       }
-
-      this.chart.chart.options.scales.yAxes[0].ticks.suggestedMax = maxValue * 1.2;
-      this.chart.chart.options.scales.yAxes[0].ticks.suggestedMin = 0;
-      this.chart.chart.options.scales.yAxes[0].ticks.stepSize = Number(
-        ((maxValue * 1.2) / 2).toFixed(0)
-      );
-      this.chart.chart.options.scales.yAxes[0].ticks.callback = (value: any) => {
-        if (value === 0) {
-          return null;
-        }
-        if (!maxValueDataUnits) {
-          return this.fillString(`${value}`);
-        }
-        return this.fillString(`${value} ${maxValueDataUnits}`);
-      };
-      this.chart.chart.update();
+      [maxValue, maxValueDataUnits] = this.convertUnits(maxValue).split(' ');
     }
+
+    const yAxesTicks = this.chart.chart.options.scales.yAxes[0].ticks;
+    yAxesTicks.suggestedMax = maxValue * extraRoom;
+    yAxesTicks.suggestedMin = 0;
+    yAxesTicks.stepSize = Number((yAxesTicks.suggestedMax / 2).toFixed(0));
+    yAxesTicks.callback = (value: any) => {
+      if (value === 0) {
+        return null;
+      }
+      if (!maxValueDataUnits) {
+        return this.fillString(`${value}`);
+      }
+      return this.fillString(`${value} ${maxValueDataUnits}`);
+    };
+    this.chartDataUnits = maxValueDataUnits || '';
+    this.chart.chart.update();
   }
 }
index 9c0dd09111088cc19d2e3b961363eafcd432664b..062e33f002b610bd639228691c32f35066ec5d5b 100644 (file)
         <ng-container *ngIf="capacity">
           <cd-dashboard-area-chart chartTitle="Used Capacity"
                                    [maxValue]="capacity.total_bytes"
-                                   dataUnits="bytes"
+                                   dataUnits="B"
                                    label="Used Capacity"
                                    [data]="queriesResults.USEDCAPACITY">
           </cd-dashboard-area-chart>
         </ng-container>
         <cd-dashboard-area-chart chartTitle="IOPS"
-                                 dataUnits="none"
+                                 dataUnits=""
                                  label="OPS"
                                  label2="IPS"
                                  [data]="queriesResults.OPS"
                                  [data2]="queriesResults.WRITELATENCY">
         </cd-dashboard-area-chart>
         <cd-dashboard-area-chart chartTitle="Client Throughput"
-                                 dataUnits="bytesPerSecond"
+                                 dataUnits="B/s"
                                  label="Read"
                                  label2="Write"
                                  [data]="queriesResults.READCLIENTTHROUGHPUT"
                                  [data2]="queriesResults.WRITECLIENTTHROUGHPUT">
         </cd-dashboard-area-chart>
         <cd-dashboard-area-chart chartTitle="Recovery Throughput"
-                                 dataUnits="bytesPerSecond"
+                                 dataUnits="B/s"
                                  label="Recovery Throughput"
                                  [data]="queriesResults.RECOVERYBYTES">
         </cd-dashboard-area-chart>
index 21b59631789bf738a2313c66dd1f42d7681fc4f4..cbd57fd2643a7b92126d2cee2efe2ccc82306eed 100644 (file)
@@ -11,14 +11,14 @@ export class DimlessBinaryPerSecondPipe implements PipeTransform {
   transform(value: any): any {
     return this.formatter.format_number(value, 1024, [
       'B/s',
-      'kB/s',
-      'MB/s',
-      'GB/s',
-      'TB/s',
-      'PB/s',
-      'EB/s',
-      'ZB/s',
-      'YB/s'
+      'KiB/s',
+      'MiB/s',
+      'GiB/s',
+      'TiB/s',
+      'PiB/s',
+      'EiB/s',
+      'ZiB/s',
+      'YiB/s'
     ]);
   }
 }
index 359c6028a593874996351c274d980f7b18b276f2..c5f13d9eb6fcc917456e1d85c082ebd3b05e0874 100644 (file)
@@ -55,6 +55,28 @@ describe('FormatterService', () => {
     });
   });
 
+  describe('formatNumberFromTo', () => {
+    const formats = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+    const formats2 = ['ns', 'μs', 'ms', 's'];
+
+    it('should test some values and data units', () => {
+      expect(service.formatNumberFromTo('0.1', 'B', 'TiB', 1024, formats)).toBe('0 TiB');
+      expect(service.formatNumberFromTo('1024', 'B', 'KiB', 1024, formats)).toBe('1 KiB');
+      expect(service.formatNumberFromTo(1000, 'mib', 'gib', 1024, formats, 3)).toBe('0.977 gib');
+      expect(service.formatNumberFromTo(1024, 'GiB', 'MiB', 1024, formats)).toBe('1048576 MiB');
+      expect(
+        service.formatNumberFromTo(23.45678 * Math.pow(1024, 3), 'B', 'GiB', 1024, formats)
+      ).toBe('23.5 GiB');
+      expect(
+        service.formatNumberFromTo(23.45678 * Math.pow(1024, 3), 'B', 'GiB', 1024, formats, 2)
+      ).toBe('23.46 GiB');
+
+      expect(service.formatNumberFromTo('128', 'ns', 'ms', 1000, formats2)).toBe('0 ms');
+      expect(service.formatNumberFromTo(128, 'ns', 'ms', 1000, formats2, 4)).toBe('0.0001 ms');
+      expect(service.formatNumberFromTo(20, 's', 'ms', 1000, formats2, 4)).toBe('20000 ms');
+    });
+  });
+
   describe('toBytes', () => {
     it('should not convert wrong values', () => {
       expect(service.toBytes('10xyz')).toBeNull();
index 790d78d21c19830d31180822f14c736cf880caa2..b212004d2b1bd2fec8764e52a628e1fed1792f2a 100644 (file)
@@ -28,6 +28,49 @@ export class FormatterService {
     return result;
   }
 
+  /**
+   * Converts a value from one set of units to another using a conversion factor
+   * @param n The value to be converted
+   * @param units The data units of the value
+   * @param targetedUnits The wanted data units to convert to
+   * @param conversionFactor The factor of convesion
+   * @param unitsArray An ordered array containing the data units
+   * @param decimals The number of decimals on the returned value
+   * @returns Returns a string of the given value formated to the targeted data units.
+   */
+  formatNumberFromTo(
+    n: any,
+    units: any,
+    targetedUnits: string,
+    conversionFactor: number,
+    unitsArray: string[],
+    decimals: number = 1
+  ): string {
+    if (_.isString(n)) {
+      n = Number(n);
+    }
+    if (!_.isNumber(n)) {
+      return '-';
+    }
+    const unitsArrayLowerCase = unitsArray.map((str) => str.toLowerCase());
+    if (
+      !unitsArrayLowerCase.includes(units.toLowerCase()) ||
+      !unitsArrayLowerCase.includes(targetedUnits.toLowerCase())
+    ) {
+      return `${n} ${units}`;
+    }
+    const index =
+      unitsArrayLowerCase.indexOf(units.toLowerCase()) -
+      unitsArrayLowerCase.indexOf(targetedUnits.toLocaleLowerCase());
+    const convertedN =
+      index > 0
+        ? n * Math.pow(conversionFactor, index)
+        : n / Math.pow(conversionFactor, Math.abs(index));
+    let result = _.round(convertedN, decimals).toString();
+    result = `${result} ${targetedUnits}`;
+    return result;
+  }
+
   /**
    * Convert the given value into bytes.
    * @param {string} value The value to be converted, e.g. 1024B, 10M, 300KiB or 1ZB.
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.spec.ts
new file mode 100644 (file)
index 0000000..5911f69
--- /dev/null
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { NumberFormatterService } from './number-formatter.service';
+
+describe('FormatToService', () => {
+  let service: NumberFormatterService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(NumberFormatterService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.ts
new file mode 100644 (file)
index 0000000..7f02d66
--- /dev/null
@@ -0,0 +1,50 @@
+import { Injectable } from '@angular/core';
+import { FormatterService } from './formatter.service';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class NumberFormatterService {
+  readonly bytesLabels = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+  readonly bytesPerSecondLabels = [
+    'B/s',
+    'KiB/s',
+    'MiB/s',
+    'GiB/s',
+    'TiB/s',
+    'PiB/s',
+    'EiB/s',
+    'ZiB/s',
+    'YiB/s'
+  ];
+  readonly secondsLabels = ['ns', 'μs', 'ms', 's', 'ks', 'Ms'];
+  readonly unitlessLabels = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
+
+  constructor(private formatter: FormatterService) {}
+
+  formatFromTo(
+    value: any,
+    units: string,
+    targetedUnits: string,
+    factor: number,
+    labels: string[]
+  ): any {
+    return this.formatter.formatNumberFromTo(value, units, targetedUnits, factor, labels);
+  }
+
+  formatBytesFromTo(value: any, units: string, targetedUnits: string): any {
+    return this.formatFromTo(value, units, targetedUnits, 1024, this.bytesLabels);
+  }
+
+  formatBytesPerSecondFromTo(value: any, units: string, targetedUnits: string): any {
+    return this.formatFromTo(value, units, targetedUnits, 1024, this.bytesPerSecondLabels);
+  }
+
+  formatSecondsFromTo(value: any, units: string, targetedUnits: string): any {
+    return this.formatFromTo(value, units, targetedUnits, 1000, this.secondsLabels);
+  }
+
+  formatUnitlessFromTo(value: any, units: string, targetedUnits: string): any {
+    return this.formatFromTo(value, units, targetedUnits, 1000, this.unitlessLabels);
+  }
+}