Grafana/UI: Replace Splitter with useSplitter hook and refactor PanelEdit snapping logic to useSnappingSplitter hook (#82895)

* Hook refactor

* Update

* Test

* Update

* Both directions work

* fixes

* refactoring

* Update

* Update

* update

* Remove consolo.log

* Update

* Fix
This commit is contained in:
Torkel Ödegaard
2024-02-21 12:34:19 +01:00
committed by GitHub
parent 6b31b1fc03
commit bfdb4625a0
10 changed files with 392 additions and 358 deletions
@@ -1,23 +0,0 @@
import { Meta, Preview, ArgTypes } from '@storybook/blocks';
import { Box, Splitter, Text } from '@grafana/ui';
<Meta title="MDX|Splitter" component={Splitter} />
# Splitter
The splitter creates two resizable panes, either horizontally or vertically.
<Preview>
<div style={{ display: 'flex', width: '900px', height: '700px' }}>
<Splitter direction="row" initialSize={0.7}>
<Box display="flex" grow={1} backgroundColor="secondary" padding={2}>
<Text color="primary">Primary</Text>
</Box>
<Box display="flex" grow={1} backgroundColor="secondary" padding={2}>
<Text color="primary">Secondary</Text>
</Box>
</Splitter>
</div>
</Preview>
<ArgTypes of={Splitter} />
@@ -1,55 +0,0 @@
import { css } from '@emotion/css';
import { Meta, StoryFn } from '@storybook/react';
import React from 'react';
import { Splitter, useTheme2 } from '@grafana/ui';
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
import mdx from './Splitter.mdx';
const meta: Meta = {
title: 'General/Layout/Splitter',
component: Splitter,
parameters: {
docs: {
page: mdx,
},
controls: {
exclude: [],
},
},
argTypes: {
initialSize: { control: { type: 'number', min: 0.1, max: 1 } },
},
};
export const Basic: StoryFn = (args) => {
const theme = useTheme2();
const paneStyles = css({
display: 'flex',
flexGrow: 1,
background: theme.colors.background.primary,
padding: theme.spacing(2),
border: `1px solid ${theme.colors.border.weak}`,
height: '100%',
});
return (
<DashboardStoryCanvas>
<div style={{ display: 'flex', width: '700px', height: '500px' }}>
<Splitter {...args}>
<div className={paneStyles}>Primary</div>
<div className={paneStyles}>Secondary</div>
</Splitter>
</div>
</DashboardStoryCanvas>
);
};
Basic.args = {
direction: 'row',
dragPosition: 'middle',
};
export default meta;
@@ -0,0 +1,34 @@
import { Meta, ArgTypes } from '@storybook/blocks';
import { Box, useSplitter, Text } from '@grafana/ui';
# useSplitter
The splitter creates two resizable panes, either horizontally or vertically.
### Usage
```tsx
import { useSplitter } from '@grafana/ui';
const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({
direction: 'row',
initialSize: 0.5,
dragPosition: 'end',
});
return (
<div {...containerProps}>
<div {...primaryProps}>
<Box display="flex" grow={1} backgroundColor="primary" padding={2}>
Primary
</Box>
</div>
<div {...splitterProps} />
<div {...secondaryProps}>
<Box display="flex" grow={1} backgroundColor="primary" padding={2}>
Secondary
</Box>
</div>
</div>
);
```
@@ -0,0 +1,71 @@
import { Meta, StoryFn } from '@storybook/react';
import React from 'react';
import { Box } from '@grafana/ui';
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
import { UseSplitterOptions, useSplitter } from './useSplitter';
import mdx from './useSplitter.mdx';
const meta: Meta = {
title: 'General/Layout/useSplitter',
parameters: {
docs: { page: mdx },
controls: {
exclude: [],
},
},
argTypes: {
initialSize: { control: { type: 'number', min: 0.1, max: 1 } },
direction: { control: { type: 'radio' }, options: ['row', 'column'] },
dragPosition: { control: { type: 'radio' }, options: ['start', 'middle', 'end'] },
hasSecondPane: { type: 'boolean', options: [true, false] },
},
};
interface StoryOptions extends UseSplitterOptions {
hasSecondPane: boolean;
}
export const Basic: StoryFn<StoryOptions> = (options) => {
const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({
...options,
});
if (!options.hasSecondPane) {
primaryProps.style.flexGrow = 1;
}
return (
<DashboardStoryCanvas>
<div style={{ display: 'flex', width: '700px', height: '500px' }}>
<div {...containerProps}>
<div {...primaryProps}>
<Box display="flex" grow={1} backgroundColor="primary" padding={2}>
Primary
</Box>
</div>
{options.hasSecondPane && (
<>
<div {...splitterProps} />
<div {...secondaryProps}>
<Box display="flex" grow={1} backgroundColor="primary" padding={2}>
Secondary
</Box>
</div>
</>
)}
</div>
</div>
</DashboardStoryCanvas>
);
};
Basic.args = {
direction: 'row',
dragPosition: 'middle',
hasSecondPane: true,
};
export default meta;
@@ -7,137 +7,19 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { DragHandlePosition, getDragStyles } from '../DragHandle/DragHandle';
export interface Props {
export interface UseSplitterOptions {
/**
* The initial size of the primary pane between 0-1, defaults to 0.5
*/
initialSize?: number;
direction?: 'row' | 'column';
direction: 'row' | 'column';
dragPosition?: DragHandlePosition;
primaryPaneStyles?: React.CSSProperties;
secondaryPaneStyles?: React.CSSProperties;
/**
* Called when ever the size of the primary pane changes
* @param flexSize (float from 0-1)
*/
onSizeChanged?: (flexSize: number, pixelSize: number) => void;
onResizing?: (flexSize: number, pixelSize: number) => void;
children: [React.ReactNode, React.ReactNode];
}
/**
* Splits two children into two resizable panes
* @alpha
*/
export function Splitter(props: Props) {
const {
direction = 'row',
initialSize = 0.5,
primaryPaneStyles,
secondaryPaneStyles,
onSizeChanged,
onResizing,
dragPosition = 'middle',
children,
} = props;
const { containerRef, firstPaneRef, minDimProp, splitterProps, secondPaneRef } = useSplitter(
direction,
onSizeChanged,
onResizing
);
const kids = React.Children.toArray(children);
const styles = useStyles2(getStyles, direction);
const dragStyles = useStyles2(getDragStyles, dragPosition);
const dragHandleStyle = direction === 'column' ? dragStyles.dragHandleHorizontal : dragStyles.dragHandleVertical;
const id = useId();
const secondAvailable = kids.length === 2;
const visibilitySecond = secondAvailable ? 'visible' : 'hidden';
let firstChildSize = initialSize;
// If second child is missing let first child have all the space
if (!children[1]) {
firstChildSize = 1;
}
return (
<div ref={containerRef} className={styles.container}>
<div
ref={firstPaneRef}
className={styles.panel}
style={{
flexGrow: clamp(firstChildSize, 0, 1),
[minDimProp]: 'min-content',
...primaryPaneStyles,
}}
id={`start-panel-${id}`}
>
{kids[0]}
</div>
{kids[1] && (
<>
<div
className={dragHandleStyle}
{...splitterProps}
role="separator"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={initialSize * 100}
aria-controls={`start-panel-${id}`}
aria-label="Pane resize widget"
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
></div>
<div
ref={secondPaneRef}
className={styles.panel}
style={{
flexGrow: clamp(1 - initialSize, 0, 1),
[minDimProp]: 'min-content',
visibility: `${visibilitySecond}`,
...secondaryPaneStyles,
}}
id={`end-panel-${id}`}
>
{kids[1]}
</div>
</>
)}
</div>
);
}
function getStyles(theme: GrafanaTheme2, direction: Props['direction']) {
return {
container: css({
display: 'flex',
flexDirection: direction === 'row' ? 'row' : 'column',
width: '100%',
flexGrow: 1,
overflow: 'hidden',
}),
panel: css({ display: 'flex', position: 'relative', flexBasis: 0 }),
dragEdge: {
second: css({
top: 0,
left: theme.spacing(-1),
bottom: 0,
position: 'absolute',
zIndex: theme.zIndex.modal,
}),
first: css({
top: 0,
left: theme.spacing(-1),
bottom: 0,
position: 'absolute',
zIndex: theme.zIndex.modal,
}),
},
};
}
const PIXELS_PER_MS = 0.3 as const;
@@ -159,11 +41,9 @@ const propsForDirection = {
},
} as const;
function useSplitter(
direction: 'row' | 'column',
onSizeChanged: Props['onSizeChanged'],
onResizing: Props['onResizing']
) {
export function useSplitter(options: UseSplitterOptions) {
const { direction, initialSize = 0.5, dragPosition = 'middle', onResizing, onSizeChanged } = options;
const handleSize = 16;
const splitterRef = useRef<HTMLDivElement | null>(null);
const firstPaneRef = useRef<HTMLDivElement | null>(null);
@@ -171,7 +51,6 @@ function useSplitter(
const containerRef = useRef<HTMLDivElement | null>(null);
const containerSize = useRef<number | null>(null);
const primarySizeRef = useRef<'1fr' | number>('1fr');
const firstPaneMeasurements = useRef<MeasureResult | undefined>(undefined);
const savedPos = useRef<string | undefined>(undefined);
@@ -197,11 +76,7 @@ function useSplitter(
const curSize = firstPaneRef.current!.getBoundingClientRect()[measurementProp];
const newDims = measureElement(firstPaneRef.current);
splitterRef.current!.ariaValueNow = `${clamp(
((curSize - newDims[minDimProp]) / (newDims[maxDimProp] - newDims[minDimProp])) * 100,
0,
100
)}`;
splitterRef.current!.ariaValueNow = ariaValue(curSize, newDims[minDimProp], newDims[maxDimProp]);
}
},
500,
@@ -239,10 +114,8 @@ function useSplitter(
firstPaneRef.current!.style.flexGrow = `${newFlex}`;
secondPaneRef.current!.style.flexGrow = `${1 - newFlex}`;
splitterRef.current!.ariaValueNow = ariaValue(newSize, dims[minDimProp], dims[maxDimProp]);
const ariaValueNow = ariaValue(newSize, dims[minDimProp], dims[maxDimProp] - dims[minDimProp]);
splitterRef.current!.ariaValueNow = `${ariaValueNow}`;
onResizing?.(newFlex, newSize);
}
},
@@ -406,7 +279,7 @@ function useSplitter(
const dim = measureElement(firstPaneRef.current);
firstPaneMeasurements.current = dim;
primarySizeRef.current = firstPaneRef.current!.getBoundingClientRect()[measurementProp];
splitterRef.current!.ariaValueNow = `${((primarySizeRef.current - dim[minDimProp]) / (dim[maxDimProp] - dim[minDimProp])) * 100}`;
splitterRef.current!.ariaValueNow = `${ariaValue(primarySizeRef.current, dim[minDimProp], dim[maxDimProp])}`;
}, [maxDimProp, measurementProp, minDimProp]);
const onBlur = useCallback(() => {
@@ -421,10 +294,32 @@ function useSplitter(
}
}, [onSizeChanged]);
const styles = useStyles2(getStyles, direction);
const dragStyles = useStyles2(getDragStyles, dragPosition);
const dragHandleStyle = direction === 'column' ? dragStyles.dragHandleHorizontal : dragStyles.dragHandleVertical;
const id = useId();
return {
containerRef,
firstPaneRef,
minDimProp,
containerProps: {
ref: containerRef,
className: styles.container,
},
primaryProps: {
ref: firstPaneRef,
className: styles.panel,
style: {
[minDimProp]: 'min-content',
flexGrow: clamp(initialSize ?? 0.5, 0, 1),
},
},
secondaryProps: {
ref: secondPaneRef,
className: styles.panel,
style: {
flexGrow: clamp(1 - initialSize, 0, 1),
[minDimProp]: 'min-content',
},
},
splitterProps: {
onPointerUp,
onPointerDown,
@@ -435,8 +330,15 @@ function useSplitter(
onBlur,
ref: splitterRef,
style: { [measurementProp]: `${handleSize}px` },
role: 'separator',
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': initialSize * 100,
'aria-controls': `start-panel-${id}`,
'aria-label': 'Pane resize widget',
tabIndex: 0,
className: dragHandleStyle,
},
secondPaneRef,
};
}
@@ -456,8 +358,10 @@ function measureElement<T extends HTMLElement>(ref: T): MeasureResult {
const savedWidth = ref.style.width;
const savedHeight = ref.style.height;
const savedFlex = ref.style.flexGrow;
document.body.style.overflow = 'hidden';
ref.style.flexGrow = '0';
const { width: minWidth, height: minHeight } = ref.getBoundingClientRect();
ref.style.flexGrow = '100';
@@ -491,3 +395,16 @@ function useResizeObserver(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
function getStyles(theme: GrafanaTheme2, direction: UseSplitterOptions['direction']) {
return {
container: css({
display: 'flex',
flexDirection: direction === 'row' ? 'row' : 'column',
width: '100%',
flexGrow: 1,
overflow: 'hidden',
}),
panel: css({ display: 'flex', position: 'relative', flexBasis: 0 }),
};
}
+2 -2
View File
@@ -264,8 +264,8 @@ export { Avatar } from './UsersIndicator/Avatar';
// Export this until we've figured out a good approach to inline form styles.
export { InlineFormLabel } from './FormLabel/FormLabel';
export { Divider } from './Divider/Divider';
export { getDragStyles } from './DragHandle/DragHandle';
export { Splitter } from './Splitter/Splitter';
export { getDragStyles, type DragHandlePosition } from './DragHandle/DragHandle';
export { useSplitter } from './Splitter/useSplitter';
export { LayoutItemContext, type LayoutItemContextProps } from './Layout/LayoutItemContext';
@@ -21,8 +21,6 @@ export interface PanelEditorState extends SceneObjectState {
isDirty?: boolean;
panelId: number;
optionsPane: PanelOptionsPane;
optionsCollapsed?: boolean;
optionsPaneSize: number;
dataPane?: PanelDataPane;
vizManager: VizPanelManager;
}
@@ -102,56 +100,8 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
sourcePanel!.parent.setState({ body: this.state.vizManager.state.panel.clone() });
}
}
public toggleOptionsPane() {
this.setState({ optionsCollapsed: !this.state.optionsCollapsed, optionsPaneSize: OPTIONS_PANE_FLEX_DEFAULT });
}
public onOptionsPaneResizing = (flexSize: number, pixelSize: number) => {
if (flexSize <= 0 && pixelSize <= 0) {
return;
}
const optionsPixelSize = (pixelSize / flexSize) * (1 - flexSize);
if (this.state.optionsCollapsed && optionsPixelSize > OPTIONS_PANE_PIXELS_MIN) {
this.setState({ optionsCollapsed: false });
}
if (!this.state.optionsCollapsed && optionsPixelSize < OPTIONS_PANE_PIXELS_MIN) {
this.setState({ optionsCollapsed: true });
}
};
public onOptionsPaneSizeChanged = (flexSize: number, pixelSize: number) => {
if (flexSize <= 0 && pixelSize <= 0) {
return;
}
const optionsPaneSize = 1 - flexSize;
const isSnappedClosed = this.state.optionsPaneSize === 0;
const fullWidth = pixelSize / flexSize;
const snapWidth = OPTIONS_PANE_PIXELS_SNAP / fullWidth;
if (this.state.optionsCollapsed) {
if (isSnappedClosed) {
this.setState({
optionsPaneSize: Math.max(optionsPaneSize, snapWidth),
optionsCollapsed: false,
});
} else {
this.setState({ optionsPaneSize: 0 });
}
} else if (isSnappedClosed) {
this.setState({ optionsPaneSize: optionsPaneSize });
}
};
}
export const OPTIONS_PANE_PIXELS_MIN = 300;
export const OPTIONS_PANE_PIXELS_SNAP = 400;
export const OPTIONS_PANE_FLEX_DEFAULT = 0.25;
export function buildPanelEditScene(panel: VizPanel): PanelEditor {
const panelClone = panel.clone();
const vizPanelMgr = new VizPanelManager(panelClone);
@@ -160,6 +110,5 @@ export function buildPanelEditScene(panel: VizPanel): PanelEditor {
panelId: getPanelIdForVizPanel(panel),
optionsPane: new PanelOptionsPane({}),
vizManager: vizPanelMgr,
optionsPaneSize: OPTIONS_PANE_FLEX_DEFAULT,
});
}
@@ -1,56 +1,109 @@
import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { Splitter, useStyles2 } from '@grafana/ui';
import { Button, ToolbarButton, useStyles2 } from '@grafana/ui';
import { NavToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils';
import { PanelEditor } from './PanelEditor';
import { useSnappingSplitter } from './splitter/useSnappingSplitter';
export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>) {
const dashboard = getDashboardSceneFor(model);
const { optionsPane, vizManager, dataPane, optionsPaneSize } = model.useState();
const { controls } = dashboard.useState();
const { optionsPane } = model.useState();
const styles = useStyles2(getStyles);
const [vizPaneStyles, optionsPaneStyles] = useMemo(() => {
if (optionsPaneSize > 0) {
return [{ flexGrow: 1 - optionsPaneSize }, { minWidth: 'unset', overflow: 'hidden', flexGrow: optionsPaneSize }];
} else {
return [{ flexGrow: 1 }, { minWidth: 'unset', flexGrow: 0 }];
}
}, [optionsPaneSize]);
const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } =
useSnappingSplitter({
direction: 'row',
dragPosition: 'end',
initialSize: 0.75,
paneOptions: {
collapseBelowPixels: 250,
snapOpenToPixels: 400,
},
});
return (
<>
<NavToolbarActions dashboard={dashboard} />
<Splitter
direction="row"
dragPosition="end"
initialSize={0.75}
primaryPaneStyles={vizPaneStyles}
secondaryPaneStyles={optionsPaneStyles}
onResizing={model.onOptionsPaneResizing}
onSizeChanged={model.onOptionsPaneSizeChanged}
>
<div className={styles.body}>
<div className={styles.canvasContent}>
{controls && <controls.Component model={controls} />}
<Splitter
direction="column"
primaryPaneStyles={{ minHeight: 0, paddingBottom: !dataPane ? 16 : 0 }}
secondaryPaneStyles={{ minHeight: 0, overflow: 'hidden' }}
dragPosition="start"
>
<vizManager.Component model={vizManager} />
{dataPane && <dataPane.Component model={dataPane} />}
</Splitter>
</div>
<div {...containerProps}>
<div {...primaryProps} className={cx(primaryProps.className, styles.body)}>
<VizAndDataPane model={model} />
</div>
{optionsPane && <optionsPane.Component model={optionsPane} />}
</Splitter>
<div {...splitterProps} />
<div {...secondaryProps} className={cx(secondaryProps.className, styles.optionsPane)}>
{splitterState.collapsed && (
<div className={styles.expandOptionsWrapper}>
<ToolbarButton
tooltip={'Open options pane'}
icon={'arrow-to-right'}
onClick={onToggleCollapse}
variant="canvas"
className={styles.rotate180}
aria-label={'Open options pane'}
/>
</div>
)}
{!splitterState.collapsed && <optionsPane.Component model={optionsPane} />}
</div>
</div>
</>
);
}
function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
const dashboard = getDashboardSceneFor(model);
const { vizManager, dataPane } = model.useState();
const { controls } = dashboard.useState();
const styles = useStyles2(getStyles);
const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } =
useSnappingSplitter({
direction: 'column',
dragPosition: 'start',
initialSize: 0.5,
paneOptions: {
collapseBelowPixels: 150,
},
});
if (!dataPane) {
primaryProps.style.flexGrow = 1;
}
return (
<>
{controls && <controls.Component model={controls} />}
<div {...containerProps}>
<div {...primaryProps}>
<vizManager.Component model={vizManager} />
</div>
{dataPane && (
<>
<div {...splitterProps} />
<div {...secondaryProps}>
{splitterState.collapsed && (
<div className={styles.expandDataPane}>
<Button
tooltip={'Open query pane'}
icon={'arrow-to-right'}
onClick={onToggleCollapse}
variant="secondary"
size="sm"
className={styles.openDataPaneButton}
aria-label={'Open query pane'}
/>
</div>
)}
{!splitterState.collapsed && <dataPane.Component model={dataPane} />}
</div>
</>
)}
</div>
</>
);
}
@@ -70,7 +123,7 @@ function getStyles(theme: GrafanaTheme2) {
label: 'body',
flexGrow: 1,
display: 'flex',
position: 'relative',
flexDirection: 'column',
minHeight: 0,
gap: '8px',
}),
@@ -81,5 +134,35 @@ function getStyles(theme: GrafanaTheme2) {
gap: theme.spacing(1),
padding: theme.spacing(2, 0, 2, 2),
}),
optionsPane: css({
flexDirection: 'column',
borderLeft: `1px solid ${theme.colors.border.weak}`,
background: theme.colors.background.primary,
}),
expandOptionsWrapper: css({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(2, 1),
}),
expandDataPane: css({
display: 'flex',
flexDirection: 'row',
padding: theme.spacing(1),
borderTop: `1px solid ${theme.colors.border.weak}`,
borderRight: `1px solid ${theme.colors.border.weak}`,
background: theme.colors.background.primary,
flexGrow: 1,
justifyContent: 'space-around',
}),
rotate180: css({
rotate: '180deg',
}),
openDataPaneButton: css({
width: theme.spacing(8),
justifyContent: 'center',
svg: {
rotate: '-90deg',
},
}),
};
}
@@ -39,47 +39,18 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
this.setState({ listMode });
};
onCollapsePane = () => {
const editor = sceneGraph.getAncestor(this, PanelEditor);
editor.toggleOptionsPane();
};
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
const { isVizPickerOpen, searchQuery, listMode } = model.useState();
const editor = sceneGraph.getAncestor(model, PanelEditor);
const { optionsCollapsed, vizManager } = editor.useState();
const vizManager = sceneGraph.getAncestor(model, PanelEditor).state.vizManager;
const { pluginId } = vizManager.state.panel.useState();
const styles = useStyles2(getStyles);
if (optionsCollapsed) {
return (
<div className={styles.pane}>
<div className={styles.top}>
<ToolbarButton
tooltip={'Open options pane'}
icon={'arrow-to-right'}
onClick={model.onCollapsePane}
variant="canvas"
className={styles.rotateIcon}
data-testid={selectors.components.PanelEditor.toggleVizOptions}
aria-label={'Open options pane'}
/>
</div>
</div>
);
}
return (
<div className={styles.pane}>
<>
{!isVizPickerOpen && (
<>
<div className={styles.top}>
<VisualizationButton
pluginId={pluginId}
onOpen={model.onToggleVizPicker}
isOpen={isVizPickerOpen}
onTogglePane={model.onCollapsePane}
/>
<VisualizationButton pluginId={pluginId} onOpen={model.onToggleVizPicker} />
<FilterInput
className={styles.searchOptions}
value={searchQuery}
@@ -93,20 +64,13 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
</>
)}
{isVizPickerOpen && <PanelVizTypePicker vizManager={vizManager} onChange={model.onToggleVizPicker} />}
</div>
</>
);
};
}
function getStyles(theme: GrafanaTheme2) {
return {
pane: css({
display: 'flex',
flexDirection: 'column',
flexGrow: '1',
borderLeft: `1px solid ${theme.colors.border.weak}`,
background: theme.colors.background.primary,
}),
top: css({
display: 'flex',
flexDirection: 'column',
@@ -137,11 +101,9 @@ function getStyles(theme: GrafanaTheme2) {
interface VisualizationButtonProps {
pluginId: string;
onOpen: () => void;
isOpen?: boolean;
onTogglePane?: () => void;
}
export function VisualizationButton({ pluginId, onOpen, isOpen, onTogglePane }: VisualizationButtonProps) {
export function VisualizationButton({ pluginId, onOpen }: VisualizationButtonProps) {
const styles = useStyles2(getVizButtonStyles);
const pluginMeta = useMemo(() => getAllPanelPluginMeta().filter((p) => p.id === pluginId)[0], [pluginId]);
@@ -160,14 +122,6 @@ export function VisualizationButton({ pluginId, onOpen, isOpen, onTogglePane }:
>
{pluginMeta.name}
</ToolbarButton>
{/* <ToolbarButton
tooltip={'Close options pane'}
icon={'arrow-to-right'}
onClick={onTogglePane}
variant="canvas"
data-testid={selectors.components.PanelEditor.toggleVizOptions}
aria-label={'Close options pane'}
/> */}
</Stack>
);
}
@@ -0,0 +1,104 @@
import React, { useCallback } from 'react';
import { DragHandlePosition, useSplitter } from '@grafana/ui';
export interface UseSnappingSplitterOptions {
/**
* The initial size of the primary pane between 0-1, defaults to 0.5
*/
initialSize?: number;
direction: 'row' | 'column';
dragPosition?: DragHandlePosition;
paneOptions: PaneOptions;
}
interface PaneOptions {
collapseBelowPixels: number;
snapOpenToPixels?: number;
}
interface PaneState {
collapsed: boolean;
snapSize?: number;
}
export function useSnappingSplitter(options: UseSnappingSplitterOptions) {
const { paneOptions } = options;
const [state, setState] = React.useState<PaneState>({ collapsed: false });
const onResizing = useCallback(
(flexSize: number, pixelSize: number) => {
if (flexSize <= 0 && pixelSize <= 0) {
return;
}
const optionsPixelSize = (pixelSize / flexSize) * (1 - flexSize);
if (state.collapsed && optionsPixelSize > paneOptions.collapseBelowPixels) {
setState({ collapsed: false });
}
if (!state.collapsed && optionsPixelSize < paneOptions.collapseBelowPixels) {
setState({ collapsed: true });
}
},
[state, paneOptions.collapseBelowPixels]
);
const onSizeChanged = useCallback(
(flexSize: number, pixelSize: number) => {
if (flexSize <= 0 && pixelSize <= 0) {
return;
}
const newSecondPaneSize = 1 - flexSize;
const isSnappedClosed = state.snapSize === 0;
const sizeOfBothPanes = pixelSize / flexSize;
const snapOpenToPixels = paneOptions.snapOpenToPixels ?? sizeOfBothPanes / 2;
const snapSize = snapOpenToPixels / sizeOfBothPanes;
if (state.collapsed) {
if (isSnappedClosed) {
setState({ snapSize: Math.max(newSecondPaneSize, snapSize), collapsed: false });
} else {
setState({ snapSize: 0, collapsed: true });
}
} else if (isSnappedClosed) {
setState({ snapSize: newSecondPaneSize, collapsed: false });
}
},
[state, paneOptions.snapOpenToPixels]
);
const onToggleCollapse = useCallback(() => {
setState({ collapsed: !state.collapsed });
}, [state.collapsed]);
const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({
...options,
onResizing,
onSizeChanged,
});
// This is to allow resizing it beyond the content dimensions
secondaryProps.style.overflow = 'hidden';
secondaryProps.style.minWidth = 'unset';
secondaryProps.style.minHeight = 'unset';
if (state.snapSize) {
primaryProps.style = {
...primaryProps.style,
flexGrow: 1 - state.snapSize,
};
secondaryProps.style.flexGrow = state.snapSize;
} else if (state.snapSize === 0) {
primaryProps.style.flexGrow = 1;
secondaryProps.style.flexGrow = 0;
secondaryProps.style.minWidth = 'unset';
secondaryProps.style.minHeight = 'unset';
secondaryProps.style.overflow = 'unset';
}
return { containerProps, primaryProps, secondaryProps, splitterProps, splitterState: state, onToggleCollapse };
}