feat(app): Add fullscreen panel view for dashboard charts (#1581)

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Drew Davis <drew.davis@clickhouse.com>
This commit is contained in:
Alok Kumar Singh 2026-01-20 20:17:16 +05:30 committed by GitHub
parent 824a19a7b9
commit 94ddc7ebaa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 168 additions and 33 deletions

View file

@ -0,0 +1,14 @@
---
'@hyperdx/app': minor
---
Add fullscreen panel view for dashboard charts
- Add YouTube-style fullscreen panel mode for dashboard charts
- Add expand button to chart hover toolbar (positioned after copy button)
- Implement 'f' keyboard shortcut to toggle fullscreen (works like YouTube)
- Support ESC key to exit fullscreen
- Works with all chart types: Line, Bar, Table, Number, Markdown, and Search
- Improved modal rendering to prevent screen shake/glitching
- Follows Mantine useHotkeys pattern for keyboard shortcuts

View file

@ -45,9 +45,10 @@ import {
Title,
Tooltip,
} from '@mantine/core';
import { useHover } from '@mantine/hooks';
import { useHotkeys, useHover } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import {
IconArrowsMaximize,
IconBell,
IconCopy,
IconDotsVertical,
@ -66,6 +67,7 @@ import EditTimeChartForm from '@/components/DBEditTimeChartForm';
import DBNumberChart from '@/components/DBNumberChart';
import DBTableChart from '@/components/DBTableChart';
import { DBTimeChart } from '@/components/DBTimeChart';
import FullscreenPanelModal from '@/components/FullscreenPanelModal';
import { SQLInlineEditorControlled } from '@/components/SQLInlineEditor';
import { TimePicker } from '@/components/TimePicker';
import {
@ -156,6 +158,9 @@ const Tile = forwardRef(
},
ref: ForwardedRef<HTMLDivElement>,
) => {
const [isFullscreen, setIsFullscreen] = useState(false);
const [isFocused, setIsFocused] = useState(false);
useEffect(() => {
if (isHighlighted) {
document
@ -164,6 +169,9 @@ const Tile = forwardRef(
}
}, [chart.id, isHighlighted]);
// YouTube-style 'f' key shortcut for fullscreen toggle
useHotkeys([['f', () => isFocused && setIsFullscreen(prev => !prev)]]);
const [queriedConfig, setQueriedConfig] = useState<
ChartConfigWithDateRange | undefined
>(undefined);
@ -266,6 +274,18 @@ const Tile = forwardRef(
>
<IconCopy size={14} />
</ActionIcon>
<ActionIcon
data-testid={`tile-fullscreen-button-${chart.id}`}
variant="subtle"
size="sm"
onClick={e => {
e.stopPropagation();
setIsFullscreen(true);
}}
title="View Fullscreen (f)"
>
<IconArrowsMaximize size={14} />
</ActionIcon>
<ActionIcon
data-testid={`tile-edit-button-${chart.id}`}
variant="subtle"
@ -307,31 +327,13 @@ const Tile = forwardRef(
[chart.config.name],
);
return (
<div
data-testid={`dashboard-tile-${chart.id}`}
className={`p-2 pt-0 ${className} d-flex flex-column bg-muted cursor-grab rounded ${
isHighlighted && 'dashboard-chart-highlighted'
}`}
id={`chart-${chart.id}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
key={chart.id}
ref={ref}
style={{
...style,
}}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onTouchEnd={onTouchEnd}
>
<Group justify="center" py={4}>
<Box bg={hovered ? 'gray' : undefined} w={100} h={2}></Box>
</Group>
<div
className="fs-7 text-muted flex-grow-1 overflow-hidden cursor-default"
onMouseDown={e => e.stopPropagation()}
>
// Render chart content (used in both tile and fullscreen views)
const renderChartContent = useCallback(
(hideToolbar: boolean = false, isFullscreenView: boolean = false) => {
const toolbar = hideToolbar ? [] : [hoverToolbar];
const keyPrefix = isFullscreenView ? 'fullscreen' : 'tile';
return (
<ErrorBoundary
onError={console.error}
fallback={
@ -343,8 +345,9 @@ const Tile = forwardRef(
{(queriedConfig?.displayType === DisplayType.Line ||
queriedConfig?.displayType === DisplayType.StackedBar) && (
<DBTimeChart
key={`${keyPrefix}-${chart.id}`}
title={title}
toolbarPrefix={[hoverToolbar]}
toolbarPrefix={toolbar}
sourceId={chart.config.source}
showDisplaySwitcher={true}
config={queriedConfig}
@ -363,8 +366,9 @@ const Tile = forwardRef(
{queriedConfig?.displayType === DisplayType.Table && (
<Box p="xs" h="100%">
<DBTableChart
key={`${keyPrefix}-${chart.id}`}
title={title}
toolbarPrefix={[hoverToolbar]}
toolbarPrefix={toolbar}
config={queriedConfig}
variant="muted"
getRowSearchLink={row =>
@ -380,25 +384,28 @@ const Tile = forwardRef(
)}
{queriedConfig?.displayType === DisplayType.Number && (
<DBNumberChart
key={`${keyPrefix}-${chart.id}`}
title={title}
toolbarPrefix={[hoverToolbar]}
toolbarPrefix={toolbar}
config={queriedConfig}
/>
)}
{queriedConfig?.displayType === DisplayType.Markdown && (
<HDXMarkdownChart
key={`${keyPrefix}-${chart.id}`}
title={title}
toolbarItems={[hoverToolbar]}
toolbarItems={toolbar}
config={queriedConfig}
/>
)}
{queriedConfig?.displayType === DisplayType.Search && (
<ChartContainer
title={title}
toolbarItems={[hoverToolbar]}
toolbarItems={toolbar}
disableReactiveContainer
>
<DBSqlRowTableWithSideBar
key={`${keyPrefix}-${chart.id}`}
enabled
sourceId={chart.config.source}
config={{
@ -426,9 +433,65 @@ const Tile = forwardRef(
</ChartContainer>
)}
</ErrorBoundary>
);
},
[
hoverToolbar,
queriedConfig,
title,
chart,
onTimeRangeSelect,
onUpdateChart,
source,
dateRange,
],
);
return (
<>
<div
data-testid={`dashboard-tile-${chart.id}`}
className={`p-2 pt-0 ${className} d-flex flex-column bg-muted cursor-grab rounded ${
isHighlighted && 'dashboard-chart-highlighted'
}`}
id={`chart-${chart.id}`}
onMouseEnter={() => {
setHovered(true);
setIsFocused(true);
}}
onMouseLeave={() => {
setHovered(false);
setIsFocused(false);
}}
key={chart.id}
ref={ref}
style={{
...style,
}}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onTouchEnd={onTouchEnd}
>
<Group justify="center" py={4}>
<Box bg={hovered ? 'gray' : undefined} w={100} h={2}></Box>
</Group>
<div
className="fs-7 text-muted flex-grow-1 overflow-hidden cursor-default"
onMouseDown={e => e.stopPropagation()}
>
{renderChartContent()}
</div>
{children}
</div>
{children}
</div>
{/* Fullscreen Modal */}
<FullscreenPanelModal
opened={isFullscreen}
onClose={() => setIsFullscreen(false)}
>
{isFullscreen && renderChartContent(true, true)}
</FullscreenPanelModal>
</>
);
},
);

View file

@ -0,0 +1,58 @@
import { Box, Modal } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
export default function FullscreenPanelModal({
opened,
onClose,
title,
children,
}: {
opened: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
}) {
// YouTube-style 'f' key to toggle fullscreen
useHotkeys([['f', () => opened && onClose()]]);
return (
<Modal
opened={opened}
onClose={onClose}
title={title}
fullScreen
transitionProps={{ transition: 'fade', duration: 200 }}
styles={{
body: {
height: 'calc(100vh - 80px)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
},
content: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
inner: {
padding: 0,
},
}}
withinPortal
trapFocus={false}
lockScroll
>
<Box
h="100%"
w="100%"
p="md"
style={{
overflow: 'auto',
position: 'relative',
}}
>
{children}
</Box>
</Modal>
);
}