]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Inconsistent tooltip timestamp format on performance card 68495/head
authorDevika Babrekar <devika.babrekar@ibm.com>
Tue, 21 Apr 2026 11:17:22 +0000 (16:47 +0530)
committerDevika Babrekar <devika.babrekar@ibm.com>
Mon, 20 Jul 2026 06:56:31 +0000 (12:26 +0530)
Fixes: https://tracker.ceph.com/issues/76169
Signed-off-by: Devika Babrekar <devika.babrekar@ibm.com>
Co-authored-by: Aashish Sharma <Aashish.Sharma1@ibm.com>
src/pybind/mgr/dashboard/frontend/jest.config.cjs
src/pybind/mgr/dashboard/frontend/src/app/shared/components/area-chart/area-chart.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/area-chart/area-chart.component.ts

index 343cedeac29aa3b2187865ca136f3748f6529bb8..77a1461783502e61e7e5cbffbfd9b0e3ab412edb 100644 (file)
@@ -14,6 +14,7 @@ const jestConfig = {
     '~/(.*)$': '<rootDir>/src/$1',
     '^@carbon/icons/es/(.*)$': '@carbon/icons/lib/$1.js',
     '^lodash-es$': 'lodash',
+    '^@carbon/charts$': '<rootDir>/node_modules/@carbon/charts/dist/index.mjs'
   },
   moduleFileExtensions: ['ts', 'html', 'js', 'json', 'mjs', 'cjs'],
   preset: 'jest-preset-angular',
index 97c7260cf7e862b06f71542f0f12f604e40f254d..0c87d65441c61f888713404bfa13df82d7d37013 100644 (file)
@@ -10,7 +10,6 @@ describe('AreaChartComponent', () => {
   let component: AreaChartComponent;
   let fixture: ComponentFixture<AreaChartComponent>;
   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 = '<div><p class="value">2024-01-01T12:30:45Z</p></div>';
+  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 =
+      '<ul class="multi-tooltip"><li><div class="datapoint-tooltip">' +
+      '<div class="label"><p>x-value</p></div><p class="value">ignored</p></div></li></ul>';
 
-    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('<p>Time</p>');
+    expect(result).not.toContain('x-value');
+    expect(result).toContain('<p class="value">ignored</p>');
   });
 
-  it('should return default HTML if tooltip data is empty', () => {
+  it('should return default HTML if tooltip has no x-value label', () => {
     const defaultHTML = '<div><p>Default</p></div>';
-    const result = component.formatChartTooltip(defaultHTML, []);
+    const result = component.formatChartTooltip(defaultHTML);
     expect(result).toBe(defaultHTML);
   });
 
index 28b33601bef69393845d7488d1636c590e61a63a..9ee3645df23a829009af3b366a408ab636d009ee 100644 (file)
@@ -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<string, number>;
 
+  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 class="value">.*?<\/p>/,
-      `<p class="value">${formattedTime}</p>`
-    );
+  formatChartTooltip(defaultHTML: string): string {
+    return defaultHTML.replace(/<p>x-value<\/p>/i, `<p>${$localize`Time`}</p>`);
   }
 
   // Uses number formatter service to convert chart value based on unit and divisor.