From 7aa70a16a54c3387b2e021763ca1d118d6785835 Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Thu, 16 Mar 2023 13:09:26 +0100 Subject: [PATCH] mgr/dashboard: fix a bug where data would plot on the graphs without converting to the data units of the graph Signed-off-by: Pedro Gonzalez Gomez --- .../dashboard-area-chart.component.ts | 153 ++++++++++-------- .../dashboard/dashboard-v3.component.html | 8 +- .../pipes/dimless-binary-per-second.pipe.ts | 16 +- .../shared/services/formatter.service.spec.ts | 22 +++ .../app/shared/services/formatter.service.ts | 43 +++++ .../services/number-formatter.service.spec.ts | 16 ++ .../services/number-formatter.service.ts | 50 ++++++ 7 files changed, 233 insertions(+), 75 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts index 3da4334210de0..0a4f2ae1e9667 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts @@ -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(); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html index bab03a1704c2c..4dc564dbe0bf2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html @@ -189,13 +189,13 @@ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary-per-second.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary-per-second.pipe.ts index 21b59631789bf..cbd57fd2643a7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary-per-second.pipe.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary-per-second.pipe.ts @@ -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' ]); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts index 359c6028a5938..c5f13d9eb6fcc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts @@ -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(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts index a4b6d427b012f..393dbed8fb1f8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts @@ -25,6 +25,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 index 0000000000000..5911f69b00b01 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.spec.ts @@ -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 index 0000000000000..7f02d66db99fb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.ts @@ -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); + } +} -- 2.39.5