Skip to content
Open
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
14 changes: 14 additions & 0 deletions .changeset/dashboard-fullscreen-panel.md
Original file line number Diff line number Diff line change
@@ -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

134 changes: 101 additions & 33 deletions packages/app/src/DBDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ import {
Title,
Tooltip,
} from '@mantine/core';
import { useHover } from '@mantine/hooks';
import { useHover, useHotkeys } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import {
IconArrowsMaximize,
IconBell,
IconCopy,
IconDotsVertical,
Expand All @@ -65,6 +66,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 {
Expand Down Expand Up @@ -155,6 +157,9 @@ const Tile = forwardRef(
},
ref: ForwardedRef<HTMLDivElement>,
) => {
const [isFullscreen, setIsFullscreen] = useState(false);
const [isFocused, setIsFocused] = useState(false);

useEffect(() => {
if (isHighlighted) {
document
Expand All @@ -163,6 +168,12 @@ const Tile = forwardRef(
}
}, [chart.id, isHighlighted]);

// YouTube-style 'f' key shortcut for fullscreen toggle
useHotkeys(
[['f', () => isFocused && setIsFullscreen(prev => !prev)]],
[isFocused],
);

const [queriedConfig, setQueriedConfig] = useState<
ChartConfigWithDateRange | undefined
>(undefined);
Expand Down Expand Up @@ -267,6 +278,19 @@ const Tile = forwardRef(
>
<IconCopy size={14} />
</Button>
<Button
data-testid={`tile-fullscreen-button-${chart.id}`}
variant="subtle"
color="gray"
size="xxs"
onClick={e => {
e.stopPropagation();
setIsFullscreen(true);
}}
title="View Fullscreen (f)"
>
<IconArrowsMaximize size={14} />
</Button>
<Button
data-testid={`tile-edit-button-${chart.id}`}
variant="subtle"
Expand Down Expand Up @@ -310,31 +334,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={
Expand All @@ -346,8 +352,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}
Expand All @@ -366,8 +373,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}
getRowSearchLink={row =>
buildTableRowSearchUrl({
Expand All @@ -382,25 +390,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={{
Expand All @@ -427,9 +438,66 @@ 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)}
title={chart.config.name}
>
{isFullscreen && renderChartContent(true, true)}
</FullscreenPanelModal>
</>
);
},
);
Expand Down
59 changes: 59 additions & 0 deletions packages/app/src/components/FullscreenPanelModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect } from 'react';
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()]], [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>
);
}