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
5 changes: 5 additions & 0 deletions .changeset/tough-ads-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/app": minor
---

feat: trace panel inline split detail

@jordan-simonovski jordan-simonovski Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we generally make patch changes for features? I feel like a split panel changing orientation from horizontal to vertical may be better suited as a minor bump semver-wise anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this should probably a minor! I'll update it

83 changes: 52 additions & 31 deletions packages/app/src/LogSidePanelElements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import * as React from 'react';
import Link from 'next/link';
import cx from 'classnames';
import { JSONTree } from 'react-json-tree';
import { Alert, Button, CloseButton, Kbd, Text, Tooltip } from '@mantine/core';
import {
Alert,
Button,
Group,
Kbd,
Modal,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import {
IconChevronDown,
IconChevronRight,
Expand Down Expand Up @@ -485,37 +494,49 @@ export const NetworkBody = ({
/**
* Keyboard shortcuts
*/
export const LogSidePanelKbdShortcuts = () => {
const [isDismissed, setDismissed] = useLocalStorage<boolean>(
'kbd-shortcuts-dismissed',
false,
);

const handleDismiss = React.useCallback(() => {
setDismissed(true);
}, [setDismissed]);

if (isDismissed) {
return null;
}

const SHORTCUTS = [
Comment thread
jordan-simonovski marked this conversation as resolved.
Resolved
{ keys: ['esc'], label: 'Close panel' },
{ keys: ['←', '→'], label: 'Navigate between events' },
{ keys: ['↑', '↓'], label: 'Navigate between events' },
{ keys: ['k', 'j'], label: 'Navigate between events' },
{ keys: ['⌘/Ctrl', 'scroll'], label: 'Zoom trace timeline' },
] as const;

export const KeyboardShortcutsModal = ({
opened,
onClose,
}: {
opened: boolean;
onClose: () => void;
}) => {
return (
<div className={styles.kbdShortcuts}>
<div className="d-flex justify-content-between align-items-center ">
<div className="d-flex align-items-center gap-3">
<div>
Use <Kbd className="me-1">←</Kbd>
<Kbd>→</Kbd> arrow keys or <Kbd className="me-1">k</Kbd>
<Kbd>j</Kbd> to move through events
</div>
<div className={styles.kbdDivider} />
<div>
<Kbd>ESC</Kbd> to close
</div>
</div>
<CloseButton aria-label="Hide" onClick={handleDismiss} />
</div>
</div>
<Modal
opened={opened}
onClose={onClose}
title="Keyboard Shortcuts"
size="sm"
centered
>
<Stack gap="sm">
{SHORTCUTS.map(({ keys, label }) => (
<Group key={label} justify="space-between">
<Group gap={4}>
{keys.map((key, i) => (
<React.Fragment key={key}>
{i > 0 && (
<Text span size="xs" c="dimmed">
+
</Text>
)}
<Kbd size="xs">{key}</Kbd>
</React.Fragment>
))}
</Group>
<Text size="sm">{label}</Text>
</Group>
))}
</Stack>
</Modal>
);
};

Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/SessionSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { IconLink, IconX } from '@tabler/icons-react';

import {
DrawerFullWidthToggle,
getInitialDrawerWidthPercent,
INITIAL_DRAWER_WIDTH_PERCENT,
} from '@/components/DrawerUtils';
import useResizable from '@/hooks/useResizable';
import {
Expand Down Expand Up @@ -54,11 +54,11 @@ export default function SessionSidePanel({
const [subDrawerOpen, setSubDrawerOpen] = useState(false);

const { size, setSize, startResize } = useResizable(
getInitialDrawerWidthPercent(),
INITIAL_DRAWER_WIDTH_PERCENT,
);
const isFullWidth = size >= 99;
const toggleFullWidth = useCallback(() => {
setSize(isFullWidth ? getInitialDrawerWidthPercent() : 100);
setSize(isFullWidth ? INITIAL_DRAWER_WIDTH_PERCENT : 100);
}, [isFullWidth, setSize]);

useHotkeys(
Expand Down
28 changes: 28 additions & 0 deletions packages/app/src/components/DBRowDataPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,34 @@ export function useRowData({
};
}

// Detects whether a normalized row carries Kubernetes resource attributes, used
// to conditionally surface the Infrastructure tab/panel. Requires the source to
// expose resource attributes; returns false (rather than throwing) on any gap.
export function rowHasK8sContext(
source: TSource | null | undefined,
normalizedRow: Record<string, any> | null | undefined,
): boolean {
try {
if (
source == null ||
!('resourceAttributesExpression' in source) ||
!source.resourceAttributesExpression ||
!normalizedRow
) {
return false;
}

const resourceAttrs = normalizedRow[ROW_DATA_ALIASES.RESOURCE_ATTRIBUTES];
return (
resourceAttrs?.['k8s.pod.uid'] != null ||
resourceAttrs?.['k8s.node.name'] != null
);
} catch (e) {
console.error(e);
return false;
}
}

export function getJSONColumnNames(meta: ResponseJSON['meta'] | undefined) {
return (
meta
Expand Down
21 changes: 19 additions & 2 deletions packages/app/src/components/DBRowJsonViewer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,28 @@ describe('DBRowJsonViewer', () => {
);
};

// Line action buttons are now icon-only; locate them by their `title`
// tooltip. Maps the friendly action name to a unique title substring.
const ACTION_TITLE: Record<string, string> = {
Search: 'search for this value only',
'Add to Filters': 'add to filters',
Column: 'column to results table',
'Copy Object': 'copy object',
'Copy Value': 'copy value',
};

const findActionButton = (line: HTMLElement, buttonText: string) => {
const needle = (ACTION_TITLE[buttonText] ?? buttonText).toLowerCase();
return within(line).getByTitle((content: string) =>
(content ?? '').toLowerCase().includes(needle),
);
};

// Helper to click a button on a line
const clickLineButton = (fieldText: string, buttonText: string) => {
const line = screen.getByText(fieldText).closest('.line')! as HTMLElement;
fireEvent.mouseEnter(line);
const button = within(line).getByText(buttonText);
const button = findActionButton(line, buttonText);
fireEvent.click(button);
};

Expand All @@ -84,7 +101,7 @@ describe('DBRowJsonViewer', () => {
.getByText(childField)
.closest('.line')! as HTMLElement;
fireEvent.mouseEnter(childLine);
const button = within(childLine).getByText(buttonText);
const button = findActionButton(childLine, buttonText);
fireEvent.click(button);
};

Expand Down
37 changes: 7 additions & 30 deletions packages/app/src/components/DBRowJsonViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,7 @@ export function DBRowJsonViewer({
) {
actions.push({
key: 'add-to-search',
label: (
<Group gap={2}>
<IconFilter size={14} />
Add to Filters
</Group>
),
label: <IconFilter size={14} />,
title: 'Add to Filters',
onClick: () => {
let filterFieldPath = fieldPath;
Expand Down Expand Up @@ -428,12 +423,7 @@ export function DBRowJsonViewer({
if (generateSearchUrl && typeof value !== 'object') {
actions.push({
key: 'search',
label: (
<Group gap={2}>
<IconSearch size={14} />
Search
</Group>
),
label: <IconSearch size={14} />,
title: 'Search for this value only',
onClick: () => {
let searchFieldPath = fieldPath;
Expand Down Expand Up @@ -532,17 +522,7 @@ export function DBRowJsonViewer({
const isIncluded = displayedColumns?.includes(columnFieldPath);
actions.push({
key: 'toggle-column',
label: isIncluded ? (
<Group gap={2}>
<IconMinus size={14} />
Column
</Group>
) : (
<Group gap={2}>
<IconPlus size={14} />
Column
</Group>
),
label: isIncluded ? <IconMinus size={14} /> : <IconPlus size={14} />,
title: isIncluded
? `Remove ${fieldPath} column from results table`
: `Add ${fieldPath} column to results table`,
Expand Down Expand Up @@ -589,18 +569,15 @@ export function DBRowJsonViewer({
if (typeof value === 'object') {
actions.push({
key: 'copy-object',
label: 'Copy Object',
label: <IconCopy size={14} />,
title: 'Copy object',
onClick: handleCopyObject,
});
} else {
actions.push({
key: 'copy-value',
label: (
<Group gap={2}>
<IconCopy size={14} />
Copy Value
</Group>
),
label: <IconCopy size={14} />,
title: 'Copy value',
onClick: async () => {
const copied = await copyTextToClipboard(
typeof value === 'string'
Expand Down
34 changes: 8 additions & 26 deletions packages/app/src/components/DBRowSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import DBRowSidePanelHeader, {
import useResizable from '@/hooks/useResizable';
import { WithClause } from '@/hooks/useRowWhere';
import useWaterfallSearchState from '@/hooks/useWaterfallSearchState';
import { LogSidePanelKbdShortcuts } from '@/LogSidePanelElements';
import { getEventBody } from '@/source';
import TabBar from '@/TabBar';
import { SearchConfig } from '@/types';
Expand All @@ -41,11 +40,11 @@ import { useZIndex, ZIndexContext } from '@/zIndex';
import ServiceMapSidePanel from './ServiceMap/ServiceMapSidePanel';
import ContextSubpanel from './ContextSidePanel';
import DBInfraPanel from './DBInfraPanel';
import { RowDataPanel, useRowData } from './DBRowDataPanel';
import { RowDataPanel, rowHasK8sContext, useRowData } from './DBRowDataPanel';
import { RowOverviewPanel } from './DBRowOverviewPanel';
import { DBSessionPanel, useSessionId } from './DBSessionPanel';
import DBTracePanel from './DBTracePanel';
import { getInitialDrawerWidthPercent } from './DrawerUtils';
import { INITIAL_DRAWER_WIDTH_PERCENT } from './DrawerUtils';

import styles from '@/../styles/LogSidePanel.module.scss';

Expand Down Expand Up @@ -289,26 +288,10 @@ const DBRowSidePanel = ({
enabled: rowId != null,
});

const hasK8sContext = useMemo(() => {
try {
if (
!('resourceAttributesExpression' in source) ||
!source?.resourceAttributesExpression ||
!normalizedRow
) {
return false;
}

const resourceAttrs = normalizedRow['__hdx_resource_attributes'];
return (
resourceAttrs?.['k8s.pod.uid'] != null ||
resourceAttrs?.['k8s.node.name'] != null
);
} catch (e) {
console.error(e);
return false;
}
}, [source, normalizedRow]);
const hasK8sContext = useMemo(
() => rowHasK8sContext(source, normalizedRow),
[source, normalizedRow],
);

const initialRowHighlightHint = useMemo(() => {
if (normalizedRow) {
Expand Down Expand Up @@ -554,7 +537,6 @@ const DBRowSidePanel = ({
</Box>
</ErrorBoundary>
)}
<LogSidePanelKbdShortcuts />
</>
);
};
Expand All @@ -572,12 +554,12 @@ export default function DBRowSidePanelErrorBoundary({
const drawerZIndex = contextZIndex + 10;

const { size, setSize, startResize } = useResizable(
getInitialDrawerWidthPercent(),
INITIAL_DRAWER_WIDTH_PERCENT,
);

const isFullWidth = size >= 99;
const toggleFullWidth = useCallback(() => {
setSize(isFullWidth ? getInitialDrawerWidthPercent() : 100);
setSize(isFullWidth ? INITIAL_DRAWER_WIDTH_PERCENT : 100);
}, [isFullWidth, setSize]);

// Keep track of sub-drawers so we can disable closing this root drawer
Expand Down
Loading
Loading