diff --git a/.changeset/help-keyboard-shortcuts-modal.md b/.changeset/help-keyboard-shortcuts-modal.md
new file mode 100644
index 00000000..825d260c
--- /dev/null
+++ b/.changeset/help-keyboard-shortcuts-modal.md
@@ -0,0 +1,9 @@
+---
+"@hyperdx/app": patch
+---
+
+feat: Add keyboard shortcuts modal from the Help menu
+
+- New **Keyboard shortcuts** item opens a modal documenting app shortcuts (command palette ⌘/Ctrl+K, search focus, time picker, tables, traces, dashboards, and more).
+- Help menu items ordered by importance (documentation and setup before shortcuts and community).
+- Shortcuts modal uses a readable width, row dividers, and **or** vs **+** labels so alternative keys are not confused with key chords.
diff --git a/packages/app/src/components/AppNav/AppNav.components.tsx b/packages/app/src/components/AppNav/AppNav.components.tsx
index 4db0de22..93485c39 100644
--- a/packages/app/src/components/AppNav/AppNav.components.tsx
+++ b/packages/app/src/components/AppNav/AppNav.components.tsx
@@ -12,6 +12,7 @@ import {
Tooltip,
UnstyledButton,
} from '@mantine/core';
+import { useDisclosure } from '@mantine/hooks';
import {
IconBook,
IconBrandDiscord,
@@ -20,6 +21,7 @@ import {
IconChevronRight,
IconChevronUp,
IconHelp,
+ IconKeyboard,
IconLogout,
IconSettings,
IconUserCog,
@@ -27,6 +29,8 @@ import {
import { IS_LOCAL_MODE } from '@/config';
+import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
+
import styles from './AppNav.module.scss';
export const AppNavContext = React.createContext<{
@@ -176,64 +180,84 @@ export const AppNavUserMenu = ({
export const AppNavHelpMenu = ({ version }: { version?: string }) => {
const { isCollapsed } = React.useContext(AppNavContext);
+ const [
+ shortcutsOpened,
+ { open: openShortcutsModal, close: closeShortcutsModal },
+ ] = useDisclosure(false);
return (
-
+ }
+ >
+ Documentation
+
+ }
+ href="https://clickhouse.com/docs/use-cases/observability/clickstack/getting-started"
+ component="a"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ Setup Instructions
+
+ }
+ onClick={openShortcutsModal}
+ >
+ Keyboard shortcuts
+
+ }
+ component="a"
+ href="https://hyperdx.io/discord"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ Discord Community
+
+
+
+
+ >
);
};
diff --git a/packages/app/src/components/AppNav/KeyboardShortcutsModal.tsx b/packages/app/src/components/AppNav/KeyboardShortcutsModal.tsx
new file mode 100644
index 00000000..8a09728a
--- /dev/null
+++ b/packages/app/src/components/AppNav/KeyboardShortcutsModal.tsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import { Divider, Group, Kbd, Modal, Stack, Text } from '@mantine/core';
+
+type ShortcutRow = {
+ keys: readonly string[];
+ label: string;
+ /** Use either key. Default is `chord` (+) for combinations pressed together. */
+ keyJoin?: 'or' | 'chord';
+};
+
+const SHORTCUTS: ShortcutRow[] = [
+ { keys: ['⌘/Ctrl', 'k'], label: 'Open command palette' },
+ {
+ keys: ['/', 's'],
+ label: 'Focus search or WHERE editor',
+ keyJoin: 'or',
+ },
+ { keys: ['d'], label: 'Open time range picker' },
+ {
+ keys: ['Enter'],
+ label: 'Apply custom time range (when time picker is open)',
+ },
+ { keys: ['⌘/Ctrl', 'f'], label: 'Find in log table' },
+ { keys: ['Enter'], label: 'Next find match (find bar open)' },
+ { keys: ['Shift', 'Enter'], label: 'Previous find match (find bar open)' },
+ {
+ keys: ['Esc'],
+ label: 'Close panel, drawer, or find bar; clear histogram bucket selection',
+ },
+ {
+ keys: ['←', '→'],
+ label: 'Move through events in log table',
+ keyJoin: 'or',
+ },
+ {
+ keys: ['↑', '↓'],
+ label: 'Move through events in log table',
+ keyJoin: 'or',
+ },
+ {
+ keys: ['k', 'j'],
+ label: 'Move through events in log table (vim-style)',
+ keyJoin: 'or',
+ },
+ { keys: ['⌘/Ctrl', 'scroll'], label: 'Zoom trace timeline' },
+ {
+ keys: ['⌥/Alt', 'click'],
+ label: 'Collapse span children (trace waterfall)',
+ },
+ { keys: ['Space'], label: 'Play / pause session replay' },
+ {
+ keys: ['f'],
+ label: 'Toggle chart fullscreen (dashboard) or exit fullscreen view',
+ },
+ { keys: ['a'], label: 'Toggle chart AI assistant (Charts page)' },
+];
+
+export const KeyboardShortcutsModal = ({
+ opened,
+ onClose,
+}: {
+ opened: boolean;
+ onClose: () => void;
+}) => {
+ return (
+
+
+ {SHORTCUTS.map(({ keys, label, keyJoin = 'chord' }, rowIndex) => (
+
+
+
+ {keys.map((key, i) => (
+
+ {i > 0 && (
+
+ {keyJoin === 'or' ? 'or' : '+'}
+
+ )}
+ {key}
+
+ ))}
+
+
+ {label}
+
+
+ {rowIndex < SHORTCUTS.length - 1 ? : null}
+
+ ))}
+
+
+ );
+};
diff --git a/packages/app/tests/e2e/core/navigation.spec.ts b/packages/app/tests/e2e/core/navigation.spec.ts
index f04e8fb8..69465399 100644
--- a/packages/app/tests/e2e/core/navigation.spec.ts
+++ b/packages/app/tests/e2e/core/navigation.spec.ts
@@ -134,14 +134,28 @@ test.describe('Navigation', { tag: ['@core'] }, () => {
const documentationItem = page.locator(
'[data-testid="documentation-menu-item"]',
);
- const discordItem = page.locator('[data-testid="discord-menu-item"]');
const setupItem = page.locator(
'[data-testid="setup-instructions-menu-item"]',
);
+ const shortcutsItem = page.locator(
+ '[data-testid="keyboard-shortcuts-menu-item"]',
+ );
+ const discordItem = page.locator('[data-testid="discord-menu-item"]');
await expect(documentationItem).toBeVisible();
- await expect(discordItem).toBeVisible();
await expect(setupItem).toBeVisible();
+ await expect(shortcutsItem).toBeVisible();
+ await expect(discordItem).toBeVisible();
+ });
+
+ await test.step('Open keyboard shortcuts from help menu', async () => {
+ const shortcutsItem = page.getByTestId('keyboard-shortcuts-menu-item');
+ await shortcutsItem.scrollIntoViewIfNeeded();
+ await shortcutsItem.click();
+
+ await expect(
+ page.getByRole('dialog', { name: 'Keyboard Shortcuts' }),
+ ).toBeVisible({ timeout: 10_000 });
});
});
});