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 ( - - - - - - + <> + + + + + + + + {!isCollapsed && Help} - {!isCollapsed && Help} - - - - - - Help{' '} - {version && ( - - v{version} - - )} - + + + + + Help{' '} + {version && ( + + v{version} + + )} + - } - > - Documentation - - } - component="a" - href="https://hyperdx.io/discord" - target="_blank" - rel="noopener noreferrer" - > - Discord Community - - } - href="https://clickhouse.com/docs/use-cases/observability/clickstack/getting-started" - component="a" - target="_blank" - rel="noopener noreferrer" - > - Setup Instructions - - - + } + > + 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 }); }); }); });