From 98c76c29c284a7cb4b626b0075e04a702b2baf91 Mon Sep 17 00:00:00 2001 From: Karl Power Date: Wed, 6 May 2026 15:47:06 +0200 Subject: [PATCH] feat: add full-width drawer toggle --- packages/app/src/SessionSidePanel.tsx | 32 ++++++++++-- .../app/src/components/DBRowSidePanel.tsx | 19 ++++++- .../src/components/DBRowSidePanelHeader.tsx | 37 ++++++++----- packages/app/src/components/DrawerUtils.tsx | 52 ++++++++++++++++++- packages/app/src/hooks/useResizable.tsx | 1 + 5 files changed, 121 insertions(+), 20 deletions(-) diff --git a/packages/app/src/SessionSidePanel.tsx b/packages/app/src/SessionSidePanel.tsx index 94895c7994..15a8ad907c 100644 --- a/packages/app/src/SessionSidePanel.tsx +++ b/packages/app/src/SessionSidePanel.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import { useHotkeys } from 'react-hotkeys-hook'; import { @@ -8,15 +8,23 @@ import { TSessionSource, TTraceSource, } from '@hyperdx/common-utils/dist/types'; -import { ActionIcon, Button, Drawer } from '@mantine/core'; +import { ActionIcon, Box, Button, Drawer, Group } from '@mantine/core'; import { notifications } from '@mantine/notifications'; import { IconLink, IconX } from '@tabler/icons-react'; +import { + DrawerFullWidthToggle, + getInitialDrawerWidthPercent, +} from '@/components/DrawerUtils'; +import useResizable from '@/hooks/useResizable'; + import { Session } from './sessions'; import SessionSubpanel from './SessionSubpanel'; import { formatDistanceToNowStrictShort } from './utils'; import { ZIndexContext } from './zIndex'; +import styles from '@/../styles/LogSidePanel.module.scss'; + export default function SessionSidePanel({ traceSource, sessionSource, @@ -42,6 +50,14 @@ export default function SessionSidePanel({ // Keep track of sub-drawers so we can disable closing this root drawer const [subDrawerOpen, setSubDrawerOpen] = useState(false); + const { size, setSize, startResize } = useResizable( + getInitialDrawerWidthPercent(), + ); + const isFullWidth = size >= 99; + const toggleFullWidth = useCallback(() => { + setSize(isFullWidth ? getInitialDrawerWidthPercent() : 100); + }, [isFullWidth, setSize]); + useHotkeys( ['esc'], () => { @@ -68,7 +84,7 @@ export default function SessionSidePanel({ } }} position="right" - size="82vw" + size={`${size}vw`} withCloseButton={false} zIndex={zIndex} styles={{ @@ -83,7 +99,9 @@ export default function SessionSidePanel({
+
@@ -102,7 +120,11 @@ export default function SessionSidePanel({ {session?.sessionCount} Events
-
+ + { @@ -124,7 +146,7 @@ export default function SessionSidePanel({ -
+
{sessionId != null ? ( diff --git a/packages/app/src/components/DBRowSidePanel.tsx b/packages/app/src/components/DBRowSidePanel.tsx index d77020865f..9efff30ed3 100644 --- a/packages/app/src/components/DBRowSidePanel.tsx +++ b/packages/app/src/components/DBRowSidePanel.tsx @@ -45,6 +45,7 @@ import { RowDataPanel, useRowData } from './DBRowDataPanel'; import { RowOverviewPanel } from './DBRowOverviewPanel'; import { DBSessionPanel, useSessionId } from './DBSessionPanel'; import DBTracePanel from './DBTracePanel'; +import { getInitialDrawerWidthPercent } from './DrawerUtils'; import styles from '@/../styles/LogSidePanel.module.scss'; @@ -109,8 +110,12 @@ const DBRowSidePanel = ({ onClose, breadcrumbPath, onBreadcrumbClick, + isFullWidth, + onToggleFullWidth, }: DBRowSidePanelProps & { setSubDrawerOpen: Dispatch>; + isFullWidth?: boolean; + onToggleFullWidth?: () => void; }) => { const { data: rowData, @@ -329,6 +334,8 @@ const DBRowSidePanel = ({ rowData={normalizedRow} breadcrumbPath={breadcrumbPath} onBreadcrumbClick={handleBreadcrumbClick} + isFullWidth={isFullWidth} + onToggleFullWidth={onToggleFullWidth} /> {/* = 99; + const toggleFullWidth = useCallback(() => { + setSize(isFullWidth ? getInitialDrawerWidthPercent() : 100); + }, [isFullWidth, setSize]); // Keep track of sub-drawers so we can disable closing this root drawer const [subDrawerOpen, setSubDrawerOpen] = useState(false); @@ -629,6 +642,8 @@ export default function DBRowSidePanelErrorBoundary({ breadcrumbPath={breadcrumbPath} setSubDrawerOpen={setSubDrawerOpen} onBreadcrumbClick={onBreadcrumbClick} + isFullWidth={isFullWidth} + onToggleFullWidth={isNestedPanel ? undefined : toggleFullWidth} /> diff --git a/packages/app/src/components/DBRowSidePanelHeader.tsx b/packages/app/src/components/DBRowSidePanelHeader.tsx index 7be8126fe5..8c6f79bf66 100644 --- a/packages/app/src/components/DBRowSidePanelHeader.tsx +++ b/packages/app/src/components/DBRowSidePanelHeader.tsx @@ -30,6 +30,7 @@ import { HighlightedAttribute, } from './DBHighlightedAttributesList'; import { RowSidePanelContext } from './DBRowSidePanel'; +import { DrawerFullWidthToggle } from './DrawerUtils'; import LogLevel from './LogLevel'; const isValidDate = (date: Date) => 'getTime' in date && !isNaN(date.getTime()); @@ -134,6 +135,8 @@ export default function DBRowSidePanelHeader({ rowData, breadcrumbPath, onBreadcrumbClick, + isFullWidth, + onToggleFullWidth, }: { date: Date; mainContent?: string; @@ -143,6 +146,8 @@ export default function DBRowSidePanelHeader({ rowData?: Record; breadcrumbPath?: BreadcrumbPath; onBreadcrumbClick?: BreadcrumbNavigationCallback; + isFullWidth?: boolean; + onToggleFullWidth?: () => void; }) { const [bodyExpanded, setBodyExpanded] = React.useState(false); const { generateSearchUrl } = useContext(RowSidePanelContext); @@ -212,18 +217,26 @@ export default function DBRowSidePanelHeader({ /> {/* Event timestamp and severity */} - - {severityText && } - {severityText && isValidDate(date) && ( - - · - - )} - {isValidDate(date) && ( - - ·{' '} - {formatDistanceToNowStrictShort(date)} ago - + + + {severityText && } + {severityText && isValidDate(date) && ( + + · + + )} + {isValidDate(date) && ( + + ·{' '} + {formatDistanceToNowStrictShort(date)} ago + + )} + + {onToggleFullWidth && ( + )} {mainContent ? ( diff --git a/packages/app/src/components/DrawerUtils.tsx b/packages/app/src/components/DrawerUtils.tsx index ef524d4af4..984dc027b1 100644 --- a/packages/app/src/components/DrawerUtils.tsx +++ b/packages/app/src/components/DrawerUtils.tsx @@ -1,6 +1,56 @@ import * as React from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; -import { Box, CloseButton, Group, Text } from '@mantine/core'; +import { + ActionIcon, + Box, + CloseButton, + Group, + Text, + Tooltip, +} from '@mantine/core'; +import { + IconLayoutSidebarRightCollapse, + IconLayoutSidebarRightExpand, +} from '@tabler/icons-react'; + +const LARGE_SCREEN_BREAKPOINT_PX = 1440; +const LARGE_SCREEN_DRAWER_WIDTH_PERCENT = 60; +const SMALL_SCREEN_DRAWER_WIDTH_PERCENT = 80; + +export function getInitialDrawerWidthPercent(): number { + if (typeof window === 'undefined') { + return SMALL_SCREEN_DRAWER_WIDTH_PERCENT; + } + + if (window.innerWidth > LARGE_SCREEN_BREAKPOINT_PX) { + return LARGE_SCREEN_DRAWER_WIDTH_PERCENT; + } + + return SMALL_SCREEN_DRAWER_WIDTH_PERCENT; +} + +export const DrawerFullWidthToggle = React.memo<{ + isFullWidth?: boolean; + onToggle: () => void; +}>(({ isFullWidth, onToggle }) => { + const label = isFullWidth ? 'Collapse panel width' : 'Expand panel width'; + return ( + + + {isFullWidth ? ( + + ) : ( + + )} + + + ); +}); export const DrawerHeader = React.memo<{ header?: React.ReactNode; diff --git a/packages/app/src/hooks/useResizable.tsx b/packages/app/src/hooks/useResizable.tsx index fcc4ac51ce..2b927a06c7 100644 --- a/packages/app/src/hooks/useResizable.tsx +++ b/packages/app/src/hooks/useResizable.tsx @@ -76,6 +76,7 @@ function useResizable( return { size: sizePercentage, + setSize: setSizePercentage, startResize, }; }