Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/services-tiles-refresh-pulse.md
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 28 additions & 10 deletions packages/app/src/components/DBHistogramChart.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -77,7 +84,12 @@ function HistogramChart({ graphResults }: { graphResults: any[] }) {
}, [data]);

return (
<ResponsiveContainer width="100%" height="100%" minWidth={0}>
<ResponsiveContainer
width="100%"
height="100%"
minWidth={0}
className={className}
>
<BarChart
width={500}
height={300}
Expand Down Expand Up @@ -212,14 +224,12 @@ export default function DBHistogramChart({
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);
Expand Down Expand Up @@ -272,18 +282,26 @@ export default function DBHistogramChart({

return (
<ChartContainer title={title} toolbarItems={toolbarItemsMemo}>
{isLoading ? (
{isLoading && !data ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError ? (
<ChartErrorState error={error} variant={errorVariant} />
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
<div
className={cx(
'd-flex h-100 w-100 align-items-center justify-content-center text-muted',
{ 'effect-pulse': isPlaceholderData },
)}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
>
No data found within time range.
</div>
) : (
<HistogramChart graphResults={buckets} />
<HistogramChart
graphResults={buckets}
className={isPlaceholderData ? 'effect-pulse' : undefined}
/>
Comment thread
Harshul1484 marked this conversation as resolved.
Resolved
)}
</ChartContainer>
);
Expand Down
34 changes: 20 additions & 14 deletions packages/app/src/components/DBListBarChart.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -285,18 +284,25 @@ export default function DBListBarChart({
) : isError ? (
<ChartErrorState error={error} variant={errorVariant} />
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
<div
className={cx(
'd-flex h-100 w-100 align-items-center justify-content-center text-muted',
{ 'effect-pulse': isPlaceholderData },
)}
>
No data found within time range.
</div>
) : (
<ListBar
data={data?.data ?? []}
columns={columns}
getRowSearchLink={getRowSearchLink}
hoverCardPosition={hoverCardPosition}
groupColumn={groupColumn}
valueColumn={valueColumn}
/>
<div className={isPlaceholderData ? 'effect-pulse' : undefined}>
<ListBar
data={data?.data ?? []}
columns={columns}
getRowSearchLink={getRowSearchLink}
hoverCardPosition={hoverCardPosition}
groupColumn={groupColumn}
valueColumn={valueColumn}
/>
</div>
)}
</ChartContainer>
);
Expand Down
79 changes: 79 additions & 0 deletions packages/app/src/components/__tests__/DBHistogramChart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,85 @@ 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(<DBHistogramChart config={baseTestConfig} />);

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(<DBHistogramChart config={baseTestConfig} />);

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(<DBHistogramChart config={baseTestConfig} />);

expect(screen.getByText('No data found within time range.')).toHaveClass(
'effect-pulse',
);
});

it('does not pulse a fresh empty result', () => {
mockUseQueriedChartConfig.mockReturnValue({
data: { data: [], meta: [] },
isLoading: false,
isPlaceholderData: false,
isError: false,
});

renderWithMantine(<DBHistogramChart config={baseTestConfig} />);

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,
isLoading: true,
isPlaceholderData: false,
isError: false,
});

renderWithMantine(<DBHistogramChart config={baseTestConfig} />);

expect(screen.getByText('Loading Chart Data...')).toBeInTheDocument();
});
});
});

describe('HISTOGRAM_BAR_COLOR', () => {
Expand Down
70 changes: 70 additions & 0 deletions packages/app/src/components/__tests__/DBListBarChart.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -169,4 +170,73 @@ describe('DBListBarChart', () => {
// Verify DateRangeIndicator was not called
expect(jest.mocked(DateRangeIndicator)).not.toHaveBeenCalled();
});

describe('refresh', () => {
const renderChart = () =>
renderWithMantine(
<DBListBarChart
config={baseTestConfig}
valueColumn="value"
groupColumn="group"
/>,
);

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',
);
});

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');
});
});
});
Loading