Compare commits

...

3 Commits

Author SHA1 Message Date
Paul Marbach
ebf77ddd9f fix scrolling while keeping resize working 2025-12-09 14:18:18 -05:00
Paul Marbach
de5d276848 fix colors 2025-12-05 16:15:21 -05:00
Paul Marbach
57168535ef fix resizing handles 2025-12-05 16:01:31 -05:00
5 changed files with 173 additions and 34 deletions

View File

@@ -71,6 +71,7 @@ export const getDragStyles = (theme: GrafanaTheme2, handlePosition?: DragHandleP
const beforeHorizontal = {
borderTop: '1px solid transparent',
width: '100%',
top: horizontalOffset,
transform: 'translateY(-50%)',
};

View File

@@ -32,10 +32,12 @@ import {
} from '@grafana/ui';
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { getExpressionIcon } from 'app/features/expressions/types';
import { FALLBACK_DOCS_LINK } from 'app/features/transformers/docs/constants';
import { getQueryRunnerFor } from '../../utils/utils';
import { usePanelDataPaneColors } from './theme';
import { QueryTransformItem } from './types';
// Props for regular item mode
@@ -69,21 +71,26 @@ interface QueryLibraryModeProps {
type QueryTransformDetailViewHeaderProps = ItemModeProps | QueryLibraryModeProps;
const ITEM_CONFIG = (theme: GrafanaTheme2) => ({
const ITEM_CONFIG = (theme: GrafanaTheme2, colors: ReturnType<typeof usePanelDataPaneColors>) => ({
query: {
color: theme.colors.primary.main,
color: colors.query.accent,
icon: 'database' as const,
},
expression: {
color: theme.visualization.getColorByName('purple'),
icon: 'calculator-alt' as const,
color: colors.expression.accent,
icon: (item: QueryTransformItem) => {
if (item.type === 'expression') {
return getExpressionIcon(item.data.type);
}
return 'calculator-alt';
},
},
transform: {
color: theme.visualization.getColorByName('orange'),
icon: 'process' as const,
color: colors.transform.accent,
icon: 'pivot' as const,
},
queryLibrary: {
color: theme.visualization.getColorByName('green'),
color: colors.query.accent,
icon: 'bookmark' as const,
},
});
@@ -101,7 +108,8 @@ function QueryLibraryHeader({
onClose: () => void;
}) {
const theme = useTheme2();
const config = useMemo(() => ITEM_CONFIG(theme).queryLibrary, [theme]);
const colors = usePanelDataPaneColors();
const config = useMemo(() => ITEM_CONFIG(theme, colors).queryLibrary, [theme, colors]);
const styles = useStyles2(getStyles, config);
return (
@@ -159,7 +167,8 @@ function ItemHeader({
debugPosition,
}: ItemModeProps) {
const theme = useTheme2();
const config = useMemo(() => ITEM_CONFIG(theme)[selectedItem.type], [theme, selectedItem.type]);
const colors = usePanelDataPaneColors();
const config = useMemo(() => ITEM_CONFIG(theme, colors)[selectedItem.type], [theme, selectedItem.type, colors]);
const styles = useStyles2(getStyles, config);
const [isEditing, setIsEditing] = useState(false);
@@ -471,7 +480,10 @@ function ItemHeader({
<div className={styles.headerContent}>
{/* Left side: Icon, Datasource, Name */}
<Stack gap={1} alignItems="center" grow={1} minWidth={0}>
<Icon name={config.icon} className={styles.icon} />
<Icon
name={typeof config.icon === 'function' ? config.icon(selectedItem) : config.icon}
className={styles.icon}
/>
{/* Datasource picker for queries */}
{selectedItem.type === 'query' && datasourceSettings && (

View File

@@ -1,6 +1,6 @@
import { css, cx } from '@emotion/css';
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd';
import { HTMLAttributes, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Fragment, HTMLAttributes, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
@@ -500,9 +500,8 @@ export const QueryTransformList = memo(
const showDebugLineAfter = isDebugMode && globalIndex === debugPosition - 1;
return (
<>
<Fragment key={item.id}>
<div
key={item.id}
className={cx(styles.cardContainer, {
[styles.cardDebugDisabled]: isDebugDisabled,
})}
@@ -584,7 +583,7 @@ export const QueryTransformList = memo(
</div>
</div>
)}
</>
</Fragment>
);
})}
{provided.placeholder}
@@ -655,9 +654,8 @@ export const QueryTransformList = memo(
const showDebugLineAfter = isDebugMode && globalIndex === debugPosition - 1;
return (
<>
<Fragment key={item.id}>
<div
key={item.id}
className={cx(styles.cardContainer, {
[styles.cardDebugDisabled]: isDebugDisabled,
})}
@@ -740,7 +738,7 @@ export const QueryTransformList = memo(
</div>
</div>
)}
</>
</Fragment>
);
})}
{provided.placeholder}
@@ -820,8 +818,6 @@ const getStyles = (theme: GrafanaTheme2, colors: ReturnType<typeof usePanelDataP
width: '100%',
maxWidth: '100%',
overflow: 'auto',
border: `1px solid ${theme.colors.border.weak}`,
borderLeft: 'none',
}),
header: css({
...barBase,

View File

@@ -17,6 +17,7 @@ import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils';
import { PanelDataSidebar, SidebarSize, SidebarState } from './PanelDataPane/PanelDataSidebar';
import { PanelEditor } from './PanelEditor';
import { SaveLibraryVizPanelModal } from './SaveLibraryVizPanelModal';
import { useHorizontalResize, useVerticalResize } from './hooks';
import { useSnappingSplitter } from './splitter/useSnappingSplitter';
import { scrollReflowMediaCondition, useScrollReflowLimit } from './useScrollReflowLimit';
@@ -27,10 +28,6 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
const [isInitiallyCollapsed, setIsCollapsed] = useEditPaneCollapsed();
const [containerRef, { height: containerHeight }] = useMeasure<HTMLDivElement>();
useEffect(() => {
console.log('PanelEditorRenderer containerHeight', containerHeight);
}, [containerHeight]);
const isScrollingLayout = useScrollReflowLimit();
const theme = useTheme2();
@@ -96,12 +93,31 @@ function VizAndDataPane({
const libraryPanel = getLibraryPanelBehavior(panel);
const { controls } = dashboard.useState();
const [sidebarState, setSidebarState] = useState<SidebarState>({ size: SidebarSize.Mini, collapsed: false });
const [vizRef, { height: vizHeight }] = useMeasure<HTMLDivElement>();
const styles = useStyles2(getStyles, sidebarState);
const isScrollingLayout = useScrollReflowLimit();
// drag-resize hooks
const {
handleRef: sidebarHandleRef,
width: sidebarWidth,
className: sidebarResizerClassName,
} = useHorizontalResize({
initialWidth: 285,
minWidth: 285,
maxWidth: 380,
});
const {
handleRef: vizHandleRef,
height: vizHeight,
className: vizResizerClassName,
} = useVerticalResize({
initialHeight: Math.max(containerHeight / 2, 200),
minHeight: 200,
maxHeight: containerHeight - 80,
});
const gridStyles = useMemo(() => {
const rows = [];
const grid = [];
@@ -132,6 +148,9 @@ function VizAndDataPane({
};
}, [controls, dataPane, sidebarState.size, vizHeight, containerHeight]);
const bottomPaneHeight = containerHeight - vizHeight - 80;
const expandedSidebarHeight = containerHeight - 16;
if (!containerHeight) {
return null;
}
@@ -146,19 +165,44 @@ function VizAndDataPane({
<div
className={cx(styles.viz, isScrollingLayout && styles.fixedSizeViz)}
ref={vizRef}
style={{ height: containerHeight / 2, maxHeight: containerHeight - 80 }}
style={{ height: vizHeight, maxHeight: containerHeight - 80 }}
>
{tableView ? <tableView.Component model={tableView} /> : <panel.Component model={panel} />}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, width: '100%' }}>
<div
style={{ height: 2, width: '100%' }}
ref={vizHandleRef}
className={vizResizerClassName}
data-testid="viz-resizer"
/>
</div>
</div>
{dataPane && (
<>
<div className={cx(styles.dataPane, isScrollingLayout && styles.fullSizeEditor)}>
<div
className={cx(styles.dataPane, isScrollingLayout && styles.fullSizeEditor)}
style={{ height: bottomPaneHeight }}
>
<dataPane.Component model={dataPane} />
</div>
<div className={styles.sidebar}>
<div
className={styles.sidebar}
style={{
height: sidebarState.size === SidebarSize.Mini ? bottomPaneHeight : expandedSidebarHeight,
width: sidebarWidth,
}}
>
<PanelDataSidebar model={dataPane} sidebarState={sidebarState} setSidebarState={setSidebarState} />
<div style={{ position: 'absolute', top: 0, bottom: 0, right: 0, height: '100%' }}>
<div
style={{ height: '100%', width: 2 }}
ref={sidebarHandleRef}
className={sidebarResizerClassName}
data-testid="sidebar-resizer"
/>
</div>
</div>
</>
)}
@@ -239,20 +283,18 @@ function getStyles(theme: GrafanaTheme2, sidebarState: SidebarState) {
}),
sidebar: css({
gridArea: 'sidebar',
overflow: 'auto',
resize: 'horizontal',
minWidth: 285,
maxWidth: 400,
overflow: 'visible',
position: 'relative',
...(sidebarState.size === SidebarSize.Mini && {
paddingLeft: theme.spacing(2),
}),
}),
viz: css({
gridArea: 'viz',
overflow: 'auto',
resize: 'vertical',
overflow: 'visible',
height: '100%',
minHeight: 100,
position: 'relative',
...(sidebarState.size === SidebarSize.Mini && {
paddingLeft: theme.spacing(2),
}),
@@ -269,6 +311,7 @@ function getStyles(theme: GrafanaTheme2, sidebarState: SidebarState) {
paddingLeft: theme.spacing(2),
}),
}),
openDataPaneButton: css({
width: theme.spacing(8),
justifyContent: 'center',

View File

@@ -0,0 +1,87 @@
import { useCallback, useState } from 'react';
import { getDragStyles, useStyles2 } from '@grafana/ui';
type UseHorizontalResizeOptions = {
initialWidth: number;
minWidth?: number;
maxWidth?: number;
};
type UseVerticalResizeOptions = {
initialHeight: number;
minHeight?: number;
maxHeight?: number;
};
export function useHorizontalResize({ initialWidth, minWidth = 0, maxWidth = Infinity }: UseHorizontalResizeOptions) {
const [width, setWidth] = useState<number>(initialWidth);
const styles = useStyles2(getDragStyles, 'middle');
const handleRef = useCallback(
(handle: HTMLElement | null) => {
let startX = 0;
let startWidth = 0;
const onMouseMove = (e: MouseEvent) => {
const delta = startX - e.clientX; // dragging left increases width of right sidebar
const newWidth = Math.min(maxWidth, Math.max(minWidth, startWidth - delta));
setWidth(newWidth);
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
const onMouseDown = (e: MouseEvent) => {
e.preventDefault();
startX = e.clientX;
startWidth = width;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
if (handle?.nodeType === Node.ELEMENT_NODE) {
handle.addEventListener('mousedown', onMouseDown);
}
},
[maxWidth, minWidth, width]
);
return { handleRef, width, setWidth, className: styles.dragHandleVertical };
}
export function useVerticalResize({ initialHeight, minHeight = 0, maxHeight = Infinity }: UseVerticalResizeOptions) {
const [height, setHeight] = useState<number>(initialHeight);
const styles = useStyles2(getDragStyles, 'middle');
const handleRef = useCallback(
(handle: HTMLElement | null) => {
let startY = 0;
let startHeight = 0;
const onMouseMove = (e: MouseEvent) => {
const delta = e.clientY - startY; // dragging down increases height
const newHeight = Math.min(maxHeight, Math.max(minHeight, startHeight + delta));
setHeight(newHeight);
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
const onMouseDown = (e: MouseEvent) => {
e.preventDefault();
startY = e.clientY;
startHeight = height;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
if (handle?.nodeType === Node.ELEMENT_NODE) {
handle.addEventListener('mousedown', onMouseDown);
}
},
[maxHeight, minHeight, height]
);
return { handleRef, height, setHeight, className: styles.dragHandleHorizontal };
}