From: Devika Babrekar Date: Tue, 21 Apr 2026 11:17:22 +0000 (+0530) Subject: mgr/dashboard: Inconsistent tooltip timestamp format on performance card X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=96ab03abe0381e1b4be52d50ea4ef16b10f23107;p=ceph.git mgr/dashboard: Inconsistent tooltip timestamp format on performance card Fixes: https://tracker.ceph.com/issues/76169 Signed-off-by: Devika Babrekar Co-authored-by: Aashish Sharma --- diff --git a/src/pybind/mgr/dashboard/frontend/jest.config.cjs b/src/pybind/mgr/dashboard/frontend/jest.config.cjs index 343cedeac29..77a14617835 100644 --- a/src/pybind/mgr/dashboard/frontend/jest.config.cjs +++ b/src/pybind/mgr/dashboard/frontend/jest.config.cjs @@ -14,6 +14,7 @@ const jestConfig = { '~/(.*)$': '/src/$1', '^@carbon/icons/es/(.*)$': '@carbon/icons/lib/$1.js', '^lodash-es$': 'lodash', + '^@carbon/charts$': '/node_modules/@carbon/charts/dist/index.mjs' }, moduleFileExtensions: ['ts', 'html', 'js', 'json', 'mjs', 'cjs'], preset: 'jest-preset-angular', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/area-chart/area-chart.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/area-chart/area-chart.component.spec.ts index 97c7260cf7e..0c87d65441c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/area-chart/area-chart.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/area-chart/area-chart.component.spec.ts @@ -10,7 +10,6 @@ describe('AreaChartComponent', () => { let component: AreaChartComponent; let fixture: ComponentFixture; let numberFormatterService: NumberFormatterService; - let datePipe: DatePipe; const mockData: ChartPoint[] = [ { @@ -41,23 +40,15 @@ describe('AreaChartComponent', () => { unitlessLabels: ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] }; - const datePipeMock = { - transform: jest.fn().mockReturnValue('01 Jan, 00:00:00') - }; - await TestBed.configureTestingModule({ imports: [ChartsModule, AreaChartComponent], - providers: [ - { provide: NumberFormatterService, useValue: numberFormatterMock }, - { provide: DatePipe, useValue: datePipeMock } - ], + providers: [DatePipe, { provide: NumberFormatterService, useValue: numberFormatterMock }], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); fixture = TestBed.createComponent(AreaChartComponent); component = fixture.componentInstance; numberFormatterService = TestBed.inject(NumberFormatterService); - datePipe = TestBed.inject(DatePipe); }); it('should mount', () => { @@ -133,23 +124,36 @@ describe('AreaChartComponent', () => { expect(component.chartOptions?.tooltip?.enabled).toBe(true); }); - it('should format tooltip with custom date format', () => { - const testDate = new Date('2024-01-01T12:30:45Z'); - const formattedDate = '01 Jan, 12:30:45'; - const defaultHTML = '

2024-01-01T12:30:45Z

'; + it('should wire tooltip customHTML with arity 2', () => { + component.chartTitle = 'Test Chart'; + component.dataUnit = 'B/s'; + component.rawData = mockData; + + (numberFormatterService.formatFromTo as jest.Mock).mockReturnValue('4.00 KiB/s'); + + fixture.detectChanges(); + component.ngOnChanges({ + rawData: new SimpleChange(null, mockData, false) + }); + + expect(component.chartOptions?.tooltip?.customHTML?.length).toBe(2); + }); - (datePipe.transform as jest.Mock).mockReturnValue(formattedDate); + it('should replace x-value label with Time in tooltip HTML', () => { + const defaultHTML = + '
  • ' + + '

    x-value

    ignored

'; - const result = component.formatChartTooltip(defaultHTML, [{ date: testDate }]); + const result = component.formatChartTooltip(defaultHTML); - expect(datePipe.transform).toHaveBeenCalledWith(testDate, 'dd MMM, HH:mm:ss'); - expect(result).toContain(formattedDate); - expect(result).not.toContain('2024-01-01T12:30:45Z'); + expect(result).toContain('

Time

'); + expect(result).not.toContain('x-value'); + expect(result).toContain('

ignored

'); }); - it('should return default HTML if tooltip data is empty', () => { + it('should return default HTML if tooltip has no x-value label', () => { const defaultHTML = '

Default

'; - const result = component.formatChartTooltip(defaultHTML, []); + const result = component.formatChartTooltip(defaultHTML); expect(result).toBe(defaultHTML); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/area-chart/area-chart.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/area-chart/area-chart.component.ts index 28b33601bef..9ee3645df23 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/area-chart/area-chart.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/area-chart/area-chart.component.ts @@ -13,11 +13,11 @@ import { ChartTabularData, ToolbarControlTypes, ScaleTypes, - ChartsModule + ChartsModule, + TickRotations } from '@carbon/charts-angular'; import merge from 'lodash.merge'; import { NumberFormatterService } from '../../services/number-formatter.service'; -import { DatePipe } from '@angular/common'; import { ChartPoint } from '../../models/area-chart-point'; import { DECIMAL, @@ -26,6 +26,12 @@ import { getDivisor, getLabels } from '../../helpers/unit-format-utils'; +import { DatePipe } from '@angular/common'; + +const DEFAULT_TICKS_COUNT = 4; +const FIVE_MINUTE_SPAN_SECONDS = 300; +const TIME_SPAN_TOLERANCE_SECONDS = 30; +const TOOLTIP_TIME_FORMAT = 'MMM d, hh:mm a'; @Component({ selector: 'cd-area-chart', @@ -59,9 +65,10 @@ export class AreaChartComponent implements OnChanges { private cdr = inject(ChangeDetectorRef); private numberFormatter: NumberFormatterService = inject(NumberFormatterService); - private datePipe = inject(DatePipe); private lastEmittedRawValues?: Record; + private datePipe = inject(DatePipe); + ngOnChanges(changes: SimpleChanges): void { if (changes['rawData'] && this.rawData?.length) { this.updateChart(); @@ -129,7 +136,22 @@ export class AreaChartComponent implements OnChanges { this.lastEmittedRawValues = { ...latestEntry.values }; } + // Calculate time span in seconds from raw data timestamps + private getTimeSpanInSeconds(): number { + if (!this.rawData || this.rawData.length < 2) { + return 0; + } + const firstTimestamp = new Date(this.rawData[0].timestamp).getTime(); + const lastTimestamp = new Date(this.rawData[this.rawData.length - 1].timestamp).getTime(); + return (lastTimestamp - firstTimestamp) / 1000; + } + private getChartOptions(max: number, labels: string[], divisor: number): AreaChartOptions { + const timeSpan = this.getTimeSpanInSeconds(); + const isFiveMinuteSpan = + timeSpan > 0 && + timeSpan >= FIVE_MINUTE_SPAN_SECONDS - TIME_SPAN_TOLERANCE_SECONDS && + timeSpan <= FIVE_MINUTE_SPAN_SECONDS + TIME_SPAN_TOLERANCE_SECONDS; return { legend: { enabled: this.legendEnabled @@ -139,8 +161,8 @@ export class AreaChartComponent implements OnChanges { mapsTo: 'date', scaleType: ScaleTypes.TIME, ticks: { - number: 4, - rotateIfSmallerThan: 0 + number: DEFAULT_TICKS_COUNT, + rotation: isFiveMinuteSpan ? TickRotations.ALWAYS : TickRotations.AUTO } }, left: { @@ -161,9 +183,18 @@ export class AreaChartComponent implements OnChanges { tooltip: { enabled: true, showTotal: false, - valueFormatter: (value: number): string => - (this.formatValueForChart(value, labels, divisor, this.decimals) || value).toString(), - customHTML: (data, defaultHTML) => this.formatChartTooltip(defaultHTML, data) + truncation: { + type: 'none' + }, + valueFormatter: (value: number | Date, label: string): string => { + if (value instanceof Date && label === 'x-value') { + return this.datePipe.transform(value, TOOLTIP_TIME_FORMAT) ?? ''; + } + return ( + this.formatValueForChart(value, labels, divisor, this.decimals) || value + ).toString(); + }, + customHTML: (_data, defaultHTML) => this.formatChartTooltip(defaultHTML) }, points: { enabled: false @@ -193,15 +224,8 @@ export class AreaChartComponent implements OnChanges { }; } - // Custom tooltip formatter to replace default timestamp with a formatted one. - formatChartTooltip(defaultHTML: string, data: { date: Date }[]): string { - if (!data?.length) return defaultHTML; - - const formattedTime = this.datePipe.transform(data[0].date, 'dd MMM, HH:mm:ss'); - return defaultHTML.replace( - /

.*?<\/p>/, - `

${formattedTime}

` - ); + formatChartTooltip(defaultHTML: string): string { + return defaultHTML.replace(/

x-value<\/p>/i, `

${$localize`Time`}

`); } // Uses number formatter service to convert chart value based on unit and divisor.