From 8174b8474acdfb81911257f7bb19405e7db18d8c Mon Sep 17 00:00:00 2001
From: Aaron Knudtson <87577305+knudtty@users.noreply.github.com>
Date: Tue, 10 Nov 2026 10:42:44 -0500
Subject: [PATCH] fix: add loading spinners when loading panels and overviews
---
.changeset/row-panel-loading-state.md | 9 +++++++
.../app/src/components/DBRowDataPanel.tsx | 25 +++++++++++++++++--
.../app/src/components/DBRowOverviewPanel.tsx | 12 ++++++++-
.../DBRowDataPanel.rowQuery.test.tsx | 15 +++++++++++
4 files changed, 58 insertions(+), 3 deletions(-)
create mode 100644 .changeset/row-panel-loading-state.md
diff --git a/.changeset/row-panel-loading-state.md b/.changeset/row-panel-loading-state.md
new file mode 100644
index 0000000000..413a85d213
--- /dev/null
+++ b/.changeset/row-panel-loading-state.md
@@ -0,0 +1,9 @@
+---
+'@hyperdx/app': patch
+---
+
+fix: show a loader while row details load
+
+Expanded search rows and the trace viewer's span details showed "Empty" or "No
+body for this event." while the row was still loading. They now show a loader
+until the row arrives.
diff --git a/packages/app/src/components/DBRowDataPanel.tsx b/packages/app/src/components/DBRowDataPanel.tsx
index 5a3803b327..2667f7f4ae 100644
--- a/packages/app/src/components/DBRowDataPanel.tsx
+++ b/packages/app/src/components/DBRowDataPanel.tsx
@@ -8,7 +8,7 @@ import {
SourceKind,
TSource,
} from '@hyperdx/common-utils/dist/types';
-import { Box } from '@mantine/core';
+import { Box, Center, Loader } from '@mantine/core';
import type { Query } from '@tanstack/react-query';
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
@@ -431,6 +431,18 @@ export function getMapColumnNames(meta: ResponseJSON['meta'] | undefined) {
);
}
+export function RowPanelLoader({
+ 'data-testid': dataTestId,
+}: {
+ 'data-testid'?: string;
+}) {
+ return (
+
+
+
+ );
+}
+
export function RowDataPanel({
source,
rowId,
@@ -448,7 +460,12 @@ export function RowDataPanel({
flush?: boolean;
'data-testid'?: string;
}) {
- const { data } = useRowData({ source, rowId, aliasWith, dateRange });
+ const { data, isLoading } = useRowData({
+ source,
+ rowId,
+ aliasWith,
+ dateRange,
+ });
const firstRow = useMemo(() => {
const firstRow = { ...(data?.data?.[0] ?? {}) };
@@ -461,6 +478,10 @@ export function RowDataPanel({
const jsonColumns = getJSONColumnNames(data?.meta);
const mapColumns = getMapColumnNames(data?.meta);
+ if (isLoading) {
+ return ;
+ }
+
return (
diff --git a/packages/app/src/components/DBRowOverviewPanel.tsx b/packages/app/src/components/DBRowOverviewPanel.tsx
index 49d6509654..09ff58c64f 100644
--- a/packages/app/src/components/DBRowOverviewPanel.tsx
+++ b/packages/app/src/components/DBRowOverviewPanel.tsx
@@ -24,6 +24,7 @@ import { BrowserEventSubpanel } from './BrowserEventSubpanel';
import {
getJSONColumnNames,
getMapColumnNames,
+ RowPanelLoader,
useRowData,
} from './DBRowDataPanel';
import { DBRowJsonViewer } from './DBRowJsonViewer';
@@ -64,7 +65,12 @@ export function RowOverviewPanel({
'data-testid'?: string;
}) {
const contentPx = flush ? 0 : 'md';
- const { data } = useRowData({ source, rowId, aliasWith, dateRange });
+ const { data, isLoading } = useRowData({
+ source,
+ rowId,
+ aliasWith,
+ dateRange,
+ });
const { onPropertyAddClick, generateSearchUrl, onOpenLinkedTrace } =
use(RowSidePanelContext);
@@ -292,6 +298,10 @@ export function RowOverviewPanel({
? JSON.stringify(firstRow['__hdx_body'])
: undefined;
+ if (isLoading) {
+ return ;
+ }
+
return (
{!hideHeader && (
diff --git a/packages/app/src/components/__tests__/DBRowDataPanel.rowQuery.test.tsx b/packages/app/src/components/__tests__/DBRowDataPanel.rowQuery.test.tsx
index cff83b9b58..19cb6fd1c4 100644
--- a/packages/app/src/components/__tests__/DBRowDataPanel.rowQuery.test.tsx
+++ b/packages/app/src/components/__tests__/DBRowDataPanel.rowQuery.test.tsx
@@ -155,6 +155,21 @@ describe('row lookup', () => {
).toEqual(['logs', 'logs_v2']);
});
+ it('shows a loader instead of an empty row while the row loads', async () => {
+ mockQueryChartConfig.mockReturnValue(new Promise(() => {}));
+
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(await screen.findByLabelText('Loading row')).toBeInTheDocument();
+ expect(mockJsonViewerData).not.toHaveBeenCalled();
+ });
+
it('runs again for a row it did not find', async () => {
mockQueryChartConfig.mockResolvedValue({ data: [], meta: [], rows: 0 });