Skip to content

Commit 2743d85

Browse files
Resizable Waterfall Traces Panel (#1328)
As requested by some users, we should have a way to resize the traces waterfall view (if there is enough trace data) so that they can see more of the trace, and still be able to click the trace events to view details https://github.com/user-attachments/assets/d1b77887-4f6e-4972-b4b4-fefd70dd344e Fixes HDX-2677
1 parent 59c6655 commit 2743d85

File tree

10 files changed

+223
-65
lines changed

10 files changed

+223
-65
lines changed

.changeset/red-frogs-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
Add ability to resize trace waterfall subpanel

packages/app/src/TimelineChart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ export default function TimelineChart({
377377
const prevScale = usePrevious(scale);
378378

379379
const initialWidthPercent = (initialLabelWidth / window.innerWidth) * 100;
380-
const { width: labelWidthPercent, startResize } = useResizable(
380+
const { size: labelWidthPercent, startResize } = useResizable(
381381
initialWidthPercent,
382382
'left',
383383
);

packages/app/src/components/DBRowSidePanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ export default function DBRowSidePanelErrorBoundary({
472472
const drawerZIndex = contextZIndex + 10;
473473

474474
const initialWidth = 80;
475-
const { width, startResize } = useResizable(initialWidth);
475+
const { size, startResize } = useResizable(initialWidth);
476476

477477
// Keep track of sub-drawers so we can disable closing this root drawer
478478
const [subDrawerOpen, setSubDrawerOpen] = useState(false);
@@ -512,7 +512,7 @@ export default function DBRowSidePanelErrorBoundary({
512512
}
513513
}}
514514
position="right"
515-
size={`${width}vw`}
515+
size={`${size}vw`}
516516
styles={{
517517
body: {
518518
padding: '0',

packages/app/src/components/DBSearchPageFilters.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,7 @@ const DBSearchPageFiltersComponent = ({
684684
isFieldPinned,
685685
pinnedFilters,
686686
} = usePinnedFilters(sourceId ?? null);
687-
const { width, startResize } = useResizable(16, 'left');
687+
const { size, startResize } = useResizable(16, 'left');
688688

689689
const { data: jsonColumns } = useJsonColumns({
690690
databaseName: chartConfig.from.databaseName,
@@ -908,7 +908,7 @@ const DBSearchPageFiltersComponent = ({
908908
);
909909

910910
return (
911-
<Box className={classes.filtersPanel} style={{ width: `${width}%` }}>
911+
<Box className={classes.filtersPanel} style={{ width: `${size}%` }}>
912912
<div className={resizeStyles.resizeHandle} onMouseDown={startResize} />
913913
<ScrollArea
914914
h="100%"

packages/app/src/components/DBTracePanel.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,6 @@ export default function DBTracePanel({
209209
)}
210210
{traceSourceData != null && eventRowWhere != null && (
211211
<>
212-
<Divider my="md" />
213212
<Group>
214213
<Text size="sm" c="dark.2" my="sm">
215214
Service Map

packages/app/src/components/DBTraceWaterfallChart.tsx

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import {
77
SourceKind,
88
TSource,
99
} from '@hyperdx/common-utils/dist/types';
10-
import { Text } from '@mantine/core';
10+
import { Divider, Text } from '@mantine/core';
1111

1212
import { ContactSupportText } from '@/components/ContactSupportText';
1313
import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery';
14+
import useResizable from '@/hooks/useResizable';
1415
import useRowWhere from '@/hooks/useRowWhere';
1516
import {
1617
getDisplayedTimestampValueExpression,
@@ -20,6 +21,7 @@ import {
2021
import TimelineChart from '@/TimelineChart';
2122

2223
import styles from '@/../styles/LogSidePanel.module.scss';
24+
import resizeStyles from '@/../styles/ResizablePanel.module.scss';
2325

2426
export type SpanRow = {
2527
Body: string;
@@ -263,6 +265,8 @@ export function DBTraceWaterfallChartContainer({
263265
body: string;
264266
};
265267
}) {
268+
const { size, startResize } = useResizable(30, 'bottom');
269+
266270
const { rows: traceRowsData, isFetching: traceIsFetching } =
267271
useEventsAroundFocus({
268272
tableSource: traceTableSource,
@@ -551,35 +555,51 @@ export function DBTraceWaterfallChartContainer({
551555
return v.id === highlightedRowWhere;
552556
});
553557

558+
const heightPx = (size / 100) * window.innerHeight;
559+
554560
return (
555561
<>
556-
{isFetching ? (
557-
<div className="my-3">Loading Traces...</div>
558-
) : rows == null ? (
559-
<div>
560-
An unknown error occurred. <ContactSupportText />
561-
</div>
562-
) : (
563-
<TimelineChart
564-
style={{
565-
overflowY: 'auto',
566-
maxHeight: 400,
567-
}}
568-
scale={1}
569-
setScale={() => {}}
570-
rowHeight={22}
571-
labelWidth={300}
572-
onClick={ts => {
573-
// onTimeClick(ts + startedAt);
574-
}}
575-
onEventClick={event => {
576-
onClick?.({ id: event.id, type: event.type ?? '' });
577-
}}
578-
cursors={[]}
579-
rows={timelineRows}
580-
initialScrollRowIndex={initialScrollRowIndex}
581-
/>
582-
)}
562+
<div
563+
style={{
564+
position: 'relative',
565+
overflow: 'hidden',
566+
maxHeight: `${heightPx}px`,
567+
}}
568+
>
569+
{isFetching ? (
570+
<div className="my-3">Loading Traces...</div>
571+
) : rows == null ? (
572+
<div>
573+
An unknown error occurred. <ContactSupportText />
574+
</div>
575+
) : (
576+
<TimelineChart
577+
style={{
578+
overflowY: 'auto',
579+
maxHeight: `${heightPx}px`,
580+
}}
581+
scale={1}
582+
setScale={() => {}}
583+
rowHeight={22}
584+
labelWidth={300}
585+
onClick={ts => {
586+
// onTimeClick(ts + startedAt);
587+
}}
588+
onEventClick={event => {
589+
onClick?.({ id: event.id, type: event.type ?? '' });
590+
}}
591+
cursors={[]}
592+
rows={timelineRows}
593+
initialScrollRowIndex={initialScrollRowIndex}
594+
/>
595+
)}
596+
</div>
597+
<Divider
598+
mt="md"
599+
className={resizeStyles.resizeYHandle}
600+
onMouseDown={startResize}
601+
style={{ position: 'relative', bottom: 0 }}
602+
/>
583603
</>
584604
);
585605
}

packages/app/src/components/__tests__/DBTraceWaterfallChart.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
3-
import { render, screen, waitFor } from '@testing-library/react';
3+
import { screen, waitFor } from '@testing-library/react';
44
import { renderHook } from '@testing-library/react';
55

66
import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery';
@@ -111,7 +111,7 @@ describe('DBTraceWaterfallChartContainer', () => {
111111
const renderComponent = (
112112
logTableSource: typeof mockLogTableSource | null = mockLogTableSource,
113113
) => {
114-
return render(
114+
return renderWithMantine(
115115
<DBTraceWaterfallChartContainer
116116
traceTableSource={mockTraceTableSource}
117117
logTableSource={logTableSource}

packages/app/src/hooks/__tests__/useResizable.test.tsx

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('useResizable', () => {
3939

4040
it('should initialize with the provided width', () => {
4141
const { result } = renderHook(() => useResizable(20));
42-
expect(result.current.width).toBe(20);
42+
expect(result.current.size).toBe(20);
4343
});
4444

4545
it('should handle right resize correctly', () => {
@@ -57,7 +57,7 @@ describe('useResizable', () => {
5757

5858
// Moving right should decrease width for right panel
5959
// Delta: 100px = 10% of window width
60-
expect(result.current.width).toBe(10); // 20 - 10
60+
expect(result.current.size).toBe(10); // 20 - 10
6161
});
6262

6363
it('should handle left resize correctly', () => {
@@ -75,7 +75,7 @@ describe('useResizable', () => {
7575

7676
// Moving right should increase width for left panel
7777
// Delta: 100px = 10% of window width
78-
expect(result.current.width).toBe(30); // 20 + 10
78+
expect(result.current.size).toBe(30); // 20 + 10
7979
});
8080

8181
it('should respect minimum width constraint', () => {
@@ -90,7 +90,7 @@ describe('useResizable', () => {
9090
fireEvent(document, moveEvent);
9191
});
9292

93-
expect(result.current.width).toBe(10); // Should not go below MIN_PANEL_WIDTH_PERCENT
93+
expect(result.current.size).toBe(10); // Should not go below MIN_PANEL_WIDTH_PERCENT
9494
});
9595

9696
it('should respect maximum width constraint', () => {
@@ -106,7 +106,7 @@ describe('useResizable', () => {
106106
});
107107

108108
// Max width should be (1000 - 25) / 1000 * 100 = 97.5%
109-
expect(result.current.width).toBeLessThanOrEqual(97.5);
109+
expect(result.current.size).toBeLessThanOrEqual(97.5);
110110
});
111111

112112
it('should cleanup event listeners on unmount', () => {
@@ -130,6 +130,103 @@ describe('useResizable', () => {
130130
expect.any(Function),
131131
);
132132
});
133+
134+
describe('vertical resizing', () => {
135+
const originalInnerHeight = window.innerHeight;
136+
const originalOffsetHeight = document.body.offsetHeight;
137+
138+
beforeEach(() => {
139+
Object.defineProperty(window, 'innerHeight', {
140+
writable: true,
141+
configurable: true,
142+
value: 1000,
143+
});
144+
145+
Object.defineProperty(document.body, 'offsetHeight', {
146+
writable: true,
147+
configurable: true,
148+
value: 1000,
149+
});
150+
});
151+
152+
afterEach(() => {
153+
Object.defineProperty(window, 'innerHeight', {
154+
writable: true,
155+
configurable: true,
156+
value: originalInnerHeight,
157+
});
158+
Object.defineProperty(document.body, 'offsetHeight', {
159+
writable: true,
160+
configurable: true,
161+
value: originalOffsetHeight,
162+
});
163+
});
164+
165+
it('should handle bottom resize correctly', () => {
166+
const { result } = renderHook(() => useResizable(20, 'bottom'));
167+
168+
act(() => {
169+
const startEvent = new MouseEvent('mousedown', { clientY: 500 });
170+
result.current.startResize(startEvent as any);
171+
172+
// Move mouse down by 100px
173+
const moveEvent = new MouseEvent('mousemove', { clientY: 600 });
174+
fireEvent(document, moveEvent);
175+
});
176+
177+
// Moving down should decrease height for bottom panel
178+
// Delta: 100px = 10% of window height
179+
expect(result.current.size).toBe(30); // 20 + 10
180+
});
181+
182+
it('should handle top resize correctly', () => {
183+
const { result } = renderHook(() => useResizable(20, 'top'));
184+
185+
act(() => {
186+
const startEvent = new MouseEvent('mousedown', { clientY: 500 });
187+
result.current.startResize(startEvent as any);
188+
189+
// Move mouse down by 100px
190+
const moveEvent = new MouseEvent('mousemove', { clientY: 600 });
191+
fireEvent(document, moveEvent);
192+
});
193+
194+
// Moving down should increase height for top panel
195+
// Delta: 100px = 10% of window height
196+
expect(result.current.size).toBe(10); // 20 - 10
197+
});
198+
199+
it('should respect minimum height constraint (bottom)', () => {
200+
const { result } = renderHook(() => useResizable(20, 'bottom'));
201+
202+
act(() => {
203+
const startEvent = new MouseEvent('mousedown', { clientY: 500 });
204+
result.current.startResize(startEvent as any);
205+
206+
// Try to resize smaller than minimum (10%)
207+
const moveEvent = new MouseEvent('mousemove', { clientY: 800 });
208+
fireEvent(document, moveEvent);
209+
});
210+
211+
expect(result.current.size).toBe(50); // Should not go below MIN_PANEL_WIDTH_PERCENT
212+
});
213+
214+
it('should respect maximum height constraint (top)', () => {
215+
const { result } = renderHook(() => useResizable(20, 'top'));
216+
217+
act(() => {
218+
const startEvent = new MouseEvent('mousedown', { clientY: 500 });
219+
result.current.startResize(startEvent as any);
220+
221+
// Try to resize larger than maximum
222+
const moveEvent = new MouseEvent('mousemove', { clientY: 1000 });
223+
fireEvent(document, moveEvent);
224+
});
225+
226+
// Max height should be (1000 - 25) / 1000 * 100 = 97.5%
227+
expect(result.current.size).toBeLessThanOrEqual(97.5);
228+
});
229+
});
133230
});
134231

135232
export {};

0 commit comments

Comments
 (0)