diff --git a/packages/grafana-ui/src/components/Splitter/Splitter.mdx b/packages/grafana-ui/src/components/Splitter/Splitter.mdx
deleted file mode 100644
index af7d3fc015b..00000000000
--- a/packages/grafana-ui/src/components/Splitter/Splitter.mdx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Meta, Preview, ArgTypes } from '@storybook/blocks';
-import { Box, Splitter, Text } from '@grafana/ui';
-
-
-
-# Splitter
-
-The splitter creates two resizable panes, either horizontally or vertically.
-
-
-
-
-
- Primary
-
-
- Secondary
-
-
-
-
-
-
diff --git a/packages/grafana-ui/src/components/Splitter/Splitter.story.tsx b/packages/grafana-ui/src/components/Splitter/Splitter.story.tsx
deleted file mode 100644
index 7248f78f3d2..00000000000
--- a/packages/grafana-ui/src/components/Splitter/Splitter.story.tsx
+++ /dev/null
@@ -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 (
-
-
-
- Primary
- Secondary
-
-
-
- );
-};
-
-Basic.args = {
- direction: 'row',
- dragPosition: 'middle',
-};
-
-export default meta;
diff --git a/packages/grafana-ui/src/components/Splitter/useSplitter.mdx b/packages/grafana-ui/src/components/Splitter/useSplitter.mdx
new file mode 100644
index 00000000000..332782e1d92
--- /dev/null
+++ b/packages/grafana-ui/src/components/Splitter/useSplitter.mdx
@@ -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 (
+
+
+
+ Primary
+
+
+
+
+
+ Secondary
+
+
+
+);
+```
diff --git a/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx b/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx
new file mode 100644
index 00000000000..ca929b79efc
--- /dev/null
+++ b/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx
@@ -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 = (options) => {
+ const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({
+ ...options,
+ });
+
+ if (!options.hasSecondPane) {
+ primaryProps.style.flexGrow = 1;
+ }
+
+ return (
+
+
+
+
+
+ Primary
+
+
+ {options.hasSecondPane && (
+ <>
+
+
+
+ Secondary
+
+
+ >
+ )}
+
+
+
+ );
+};
+
+Basic.args = {
+ direction: 'row',
+ dragPosition: 'middle',
+ hasSecondPane: true,
+};
+
+export default meta;
diff --git a/packages/grafana-ui/src/components/Splitter/Splitter.tsx b/packages/grafana-ui/src/components/Splitter/useSplitter.ts
similarity index 78%
rename from packages/grafana-ui/src/components/Splitter/Splitter.tsx
rename to packages/grafana-ui/src/components/Splitter/useSplitter.ts
index ded1e13d65f..5f3b0b8e4a8 100644
--- a/packages/grafana-ui/src/components/Splitter/Splitter.tsx
+++ b/packages/grafana-ui/src/components/Splitter/useSplitter.ts
@@ -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 (
-
-
- {kids[0]}
-
-
- {kids[1] && (
- <>
-
-
-
- {kids[1]}
-
- >
- )}
-
- );
-}
-
-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(null);
const firstPaneRef = useRef(null);
@@ -171,7 +51,6 @@ function useSplitter(
const containerRef = useRef(null);
const containerSize = useRef(null);
const primarySizeRef = useRef<'1fr' | number>('1fr');
-
const firstPaneMeasurements = useRef(undefined);
const savedPos = useRef(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(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 }),
+ };
+}
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index 435d63f2442..ec094bea02e 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -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';
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx
index 6b2ceaa457e..91840641603 100644
--- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx
@@ -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 {
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,
});
}
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx
index a464b0a538b..2b81f59779d 100644
--- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx
@@ -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) {
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 (
<>
-
-
-
- {controls && }
-
-
- {dataPane && }
-
-
+
+
+
- {optionsPane &&
}
-
+
+
+ {splitterState.collapsed && (
+
+
+
+ )}
+ {!splitterState.collapsed &&
}
+
+
+ >
+ );
+}
+
+function VizAndDataPane({ model }: SceneComponentProps
) {
+ 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 && }
+
+
+
+
+ {dataPane && (
+ <>
+
+
+ {splitterState.collapsed && (
+
+
+
+ )}
+ {!splitterState.collapsed &&
}
+
+ >
+ )}
+
>
);
}
@@ -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',
+ },
+ }),
};
}
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx
index 98d37e26d2b..0678f10734e 100644
--- a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx
@@ -39,47 +39,18 @@ export class PanelOptionsPane extends SceneObjectBase {
this.setState({ listMode });
};
- onCollapsePane = () => {
- const editor = sceneGraph.getAncestor(this, PanelEditor);
- editor.toggleOptionsPane();
- };
-
static Component = ({ model }: SceneComponentProps) => {
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 (
-
- );
- }
-
return (
-
+ <>
{!isVizPickerOpen && (
<>
-
+
{
>
)}
{isVizPickerOpen && }
-
+ >
);
};
}
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}
- {/*
*/}
);
}
diff --git a/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts b/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts
new file mode 100644
index 00000000000..95e78cac99c
--- /dev/null
+++ b/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts
@@ -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
({ 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 };
+}