mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
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:
parent
824a19a7b9
commit
94ddc7ebaa
3 changed files with 168 additions and 33 deletions
14
.changeset/dashboard-fullscreen-panel.md
Normal file
14
.changeset/dashboard-fullscreen-panel.md
Normal 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
|
||||
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
58
packages/app/src/components/FullscreenPanelModal.tsx
Normal file
58
packages/app/src/components/FullscreenPanelModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue