From bc3e38e57a8731fd7a0d5df00d45e707b208d7e4 Mon Sep 17 00:00:00 2001 From: Harshul Rathod Date: Sat, 26 Sep 2026 03:29:12 +0530 Subject: [PATCH 1/2] fix(app): pulse the list-bar and histogram charts while they refresh --- .changeset/services-tiles-refresh-pulse.md | 11 ++++ .../app/src/components/DBHistogramChart.tsx | 38 ++++++++--- .../app/src/components/DBListBarChart.tsx | 34 ++++++---- .../__tests__/DBHistogramChart.test.tsx | 64 +++++++++++++++++++ .../__tests__/DBListBarChart.test.tsx | 55 ++++++++++++++++ 5 files changed, 178 insertions(+), 24 deletions(-) create mode 100644 .changeset/services-tiles-refresh-pulse.md diff --git a/.changeset/services-tiles-refresh-pulse.md b/.changeset/services-tiles-refresh-pulse.md new file mode 100644 index 0000000000..c3ecf6c2a8 --- /dev/null +++ b/.changeset/services-tiles-refresh-pulse.md @@ -0,0 +1,11 @@ +--- +'@hyperdx/app': patch +--- + +fix: pulse the list-bar and histogram charts while they refresh + +The Services dashboard's list-bar and latency histogram charts kept their +previous data during a refresh but gave no sign that new data was loading, +and the histogram could briefly swap its chart for "Loading Chart Data...". +Both now keep their previous data on screen and pulse while the refetch runs, +like the other charts. diff --git a/packages/app/src/components/DBHistogramChart.tsx b/packages/app/src/components/DBHistogramChart.tsx index 6a4403cfe0..2244461c4a 100644 --- a/packages/app/src/components/DBHistogramChart.tsx +++ b/packages/app/src/components/DBHistogramChart.tsx @@ -1,4 +1,5 @@ import { memo, useEffect, useMemo, useState } from 'react'; +import cx from 'classnames'; import { omit } from 'lodash'; import { useHotkeys } from 'react-hotkeys-hook'; import { @@ -47,7 +48,13 @@ export function resolvePinnedBarIndex( return Number.isInteger(idx) && idx >= 0 ? idx : undefined; } -function HistogramChart({ graphResults }: { graphResults: any[] }) { +function HistogramChart({ + graphResults, + className, +}: { + graphResults: any[]; + className?: string; +}) { const data = useMemo(() => { return ( graphResults?.map((result: any) => { @@ -77,7 +84,12 @@ function HistogramChart({ graphResults }: { graphResults: any[] }) { }, [data]); return ( - + prev, queryKey: [queryKeyPrefix, queriedConfig], enabled, - }, - ); + }); const { data: mvOptimizationData } = useMVOptimizationExplanation(queriedConfig); @@ -272,18 +282,26 @@ export default function DBHistogramChart({ return ( - {isLoading ? ( + {isLoading && !data ? (
Loading Chart Data...
) : isError ? ( ) : data?.data.length === 0 ? ( -
+
No data found within time range.
) : ( - + )} ); diff --git a/packages/app/src/components/DBListBarChart.tsx b/packages/app/src/components/DBListBarChart.tsx index f008b346bd..ef463ff1df 100644 --- a/packages/app/src/components/DBListBarChart.tsx +++ b/packages/app/src/components/DBListBarChart.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import Link from 'next/link'; +import cx from 'classnames'; import { omit } from 'lodash'; import { BuilderChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import type { FloatingPosition } from '@mantine/core'; @@ -202,14 +203,12 @@ export default function DBListBarChart({ errorVariant?: ChartErrorStateVariant; }) { const queriedConfig = omit(config, ['granularity']); - const { data, isLoading, isError, error } = useQueriedChartConfig( - queriedConfig, - { + const { data, isLoading, isPlaceholderData, isError, error } = + useQueriedChartConfig(queriedConfig, { placeholderData: (prev: any) => prev, queryKey: [queryKeyPrefix, queriedConfig], enabled, - }, - ); + }); const { data: mvOptimizationData } = useMVOptimizationExplanation(queriedConfig); @@ -285,18 +284,25 @@ export default function DBListBarChart({ ) : isError ? ( ) : data?.data.length === 0 ? ( -
+
No data found within time range.
) : ( - +
+ +
)} ); diff --git a/packages/app/src/components/__tests__/DBHistogramChart.test.tsx b/packages/app/src/components/__tests__/DBHistogramChart.test.tsx index 6cced53288..8e1534e966 100644 --- a/packages/app/src/components/__tests__/DBHistogramChart.test.tsx +++ b/packages/app/src/components/__tests__/DBHistogramChart.test.tsx @@ -156,6 +156,70 @@ describe('DBHistogramChart', () => { // Verify DateRangeIndicator was not called expect(jest.mocked(DateRangeIndicator)).not.toHaveBeenCalled(); }); + + describe('refresh', () => { + const buckets = { data: [{ data: [[0, 10, 5]] }], meta: [] }; + const chart = () => + document.querySelector('.recharts-responsive-container'); + + it('keeps the previous histogram on screen and pulses it while a refresh loads', () => { + // The new range is still loading behind the previous buckets. + mockUseQueriedChartConfig.mockReturnValue({ + data: buckets, + isLoading: true, + isPlaceholderData: true, + isError: false, + }); + + renderWithMantine(); + + expect( + screen.queryByText('Loading Chart Data...'), + ).not.toBeInTheDocument(); + expect(chart()).toHaveClass('effect-pulse'); + }); + + it('does not pulse once fresh buckets load', () => { + mockUseQueriedChartConfig.mockReturnValue({ + data: buckets, + isLoading: false, + isPlaceholderData: false, + isError: false, + }); + + renderWithMantine(); + + expect(chart()).not.toHaveClass('effect-pulse'); + }); + + it('pulses the empty state while a refresh loads', () => { + mockUseQueriedChartConfig.mockReturnValue({ + data: { data: [], meta: [] }, + isLoading: false, + isPlaceholderData: true, + isError: false, + }); + + renderWithMantine(); + + expect(screen.getByText('No data found within time range.')).toHaveClass( + 'effect-pulse', + ); + }); + + it('shows the loading state before the first buckets arrive', () => { + mockUseQueriedChartConfig.mockReturnValue({ + data: undefined, + isLoading: true, + isPlaceholderData: false, + isError: false, + }); + + renderWithMantine(); + + expect(screen.getByText('Loading Chart Data...')).toBeInTheDocument(); + }); + }); }); describe('HISTOGRAM_BAR_COLOR', () => { diff --git a/packages/app/src/components/__tests__/DBListBarChart.test.tsx b/packages/app/src/components/__tests__/DBListBarChart.test.tsx index 6c6a30fa28..e936531df8 100644 --- a/packages/app/src/components/__tests__/DBListBarChart.test.tsx +++ b/packages/app/src/components/__tests__/DBListBarChart.test.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { screen } from '@testing-library/react'; import DateRangeIndicator from '@/components/charts/DateRangeIndicator'; import DBListBarChart from '@/components/DBListBarChart'; @@ -169,4 +170,58 @@ describe('DBListBarChart', () => { // Verify DateRangeIndicator was not called expect(jest.mocked(DateRangeIndicator)).not.toHaveBeenCalled(); }); + + describe('refresh', () => { + const renderChart = () => + renderWithMantine( + , + ); + + it('pulses the previous bars while a refresh loads', () => { + mockUseQueriedChartConfig.mockReturnValue({ + data: { data: [{ group: 'checkout', value: 100 }] }, + isLoading: false, + isPlaceholderData: true, + isError: false, + }); + + renderChart(); + + expect( + screen.getByText('checkout').closest('.effect-pulse'), + ).toBeInTheDocument(); + }); + + it('does not pulse once fresh bars load', () => { + mockUseQueriedChartConfig.mockReturnValue({ + data: { data: [{ group: 'checkout', value: 100 }] }, + isLoading: false, + isPlaceholderData: false, + isError: false, + }); + + renderChart(); + + expect(screen.getByText('checkout').closest('.effect-pulse')).toBeNull(); + }); + + it('pulses the empty state while a refresh loads', () => { + mockUseQueriedChartConfig.mockReturnValue({ + data: { data: [] }, + isLoading: false, + isPlaceholderData: true, + isError: false, + }); + + renderChart(); + + expect(screen.getByText('No data found within time range.')).toHaveClass( + 'effect-pulse', + ); + }); + }); }); From 8e7222e2ff4427c4233a688bf5ccac0e9ae9ba58 Mon Sep 17 00:00:00 2001 From: Harshul Rathod Date: Sat, 26 Sep 2026 03:51:47 +0530 Subject: [PATCH 2/2] test(app): check a fresh empty chart result doesn't pulse --- .../__tests__/DBHistogramChart.test.tsx | 15 +++++++++++++++ .../components/__tests__/DBListBarChart.test.tsx | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/app/src/components/__tests__/DBHistogramChart.test.tsx b/packages/app/src/components/__tests__/DBHistogramChart.test.tsx index 8e1534e966..1b242fa12f 100644 --- a/packages/app/src/components/__tests__/DBHistogramChart.test.tsx +++ b/packages/app/src/components/__tests__/DBHistogramChart.test.tsx @@ -207,6 +207,21 @@ describe('DBHistogramChart', () => { ); }); + it('does not pulse a fresh empty result', () => { + mockUseQueriedChartConfig.mockReturnValue({ + data: { data: [], meta: [] }, + isLoading: false, + isPlaceholderData: false, + isError: false, + }); + + renderWithMantine(); + + expect( + screen.getByText('No data found within time range.'), + ).not.toHaveClass('effect-pulse'); + }); + it('shows the loading state before the first buckets arrive', () => { mockUseQueriedChartConfig.mockReturnValue({ data: undefined, diff --git a/packages/app/src/components/__tests__/DBListBarChart.test.tsx b/packages/app/src/components/__tests__/DBListBarChart.test.tsx index e936531df8..9fbb6c218f 100644 --- a/packages/app/src/components/__tests__/DBListBarChart.test.tsx +++ b/packages/app/src/components/__tests__/DBListBarChart.test.tsx @@ -223,5 +223,20 @@ describe('DBListBarChart', () => { 'effect-pulse', ); }); + + it('does not pulse a fresh empty result', () => { + mockUseQueriedChartConfig.mockReturnValue({ + data: { data: [] }, + isLoading: false, + isPlaceholderData: false, + isError: false, + }); + + renderChart(); + + expect( + screen.getByText('No data found within time range.'), + ).not.toHaveClass('effect-pulse'); + }); }); });