grafana/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx
Kristina c3e4f1f876
Explore: Add resize to split view, with Min/Max button (#54420)
* Add split resize to Explore without keeping width in state

* debug commit

* ugly hack around split lib only supporting one child

* Use SplitView to accomodate one or two elements, remove debug code, fix test

* More cleanup, fix state action

* Fix even split from manual size scenario

* cleanup

* Add new state elements to test

* Handle scrollable on internal element for virtualized lists

* Left align overflow button for explore

* Change min/max buttons

* Apply suggestions from code review

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Add more suggestions from the code review

* Fix problems tests found

* commit broken test with debug info

* Add test, remove debug code

* Remove second get of panes

* Remove second get of panes

Co-authored-by: Elfo404 <me@giordanoricci.com>
2022-09-23 08:20:33 -05:00

205 lines
4.9 KiB
TypeScript

import { css, cx } from '@emotion/css';
import React, { createRef, MutableRefObject, PureComponent, ReactNode } from 'react';
import SplitPane from 'react-split-pane';
import { GrafanaTheme } from '@grafana/data';
import { stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config';
import { SplitView } from './SplitView';
enum Pane {
Right,
Top,
}
interface Props {
leftPaneComponents: ReactNode[] | ReactNode;
rightPaneComponents: ReactNode;
uiState: { topPaneSize: number; rightPaneSize: number };
rightPaneVisible?: boolean;
updateUiState: (uiState: { topPaneSize?: number; rightPaneSize?: number }) => void;
}
export class SplitPaneWrapper extends PureComponent<Props> {
rafToken: MutableRefObject<number | null> = createRef();
static defaultProps = {
rightPaneVisible: true,
};
componentDidMount() {
window.addEventListener('resize', this.updateSplitPaneSize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateSplitPaneSize);
}
updateSplitPaneSize = () => {
if (this.rafToken.current !== undefined) {
window.cancelAnimationFrame(this.rafToken.current!);
}
this.rafToken.current = window.requestAnimationFrame(() => {
this.forceUpdate();
});
};
onDragFinished = (pane: Pane, size?: number) => {
document.body.style.cursor = 'auto';
// When the drag handle is just clicked size is undefined
if (!size) {
return;
}
const { updateUiState } = this.props;
if (pane === Pane.Top) {
updateUiState({
topPaneSize: size / window.innerHeight,
});
} else {
updateUiState({
rightPaneSize: size / window.innerWidth,
});
}
};
onDragStarted = () => {
document.body.style.cursor = 'row-resize';
};
renderHorizontalSplit() {
const { leftPaneComponents, uiState } = this.props;
const styles = getStyles(config.theme);
const topPaneSize = uiState.topPaneSize >= 1 ? uiState.topPaneSize : uiState.topPaneSize * window.innerHeight;
/*
Guesstimate the height of the browser window minus
panel toolbar and editor toolbar (~120px). This is to prevent resizing
the preview window beyond the browser window.
*/
if (Array.isArray(leftPaneComponents)) {
return (
<SplitPane
split="horizontal"
maxSize={-200}
primary="first"
size={topPaneSize}
pane2Style={{ minHeight: 0 }}
resizerClassName={styles.resizerH}
onDragStarted={this.onDragStarted}
onDragFinished={(size) => this.onDragFinished(Pane.Top, size)}
>
{leftPaneComponents}
</SplitPane>
);
}
return <div className={styles.singleLeftPane}>{leftPaneComponents}</div>;
}
render() {
const { rightPaneVisible, rightPaneComponents, uiState } = this.props;
// Limit options pane width to 90% of screen.
// Need to handle when width is relative. ie a percentage of the viewport
const rightPaneSize =
uiState.rightPaneSize <= 1 ? uiState.rightPaneSize * window.innerWidth : uiState.rightPaneSize;
if (!rightPaneVisible) {
return this.renderHorizontalSplit();
}
return (
<SplitView uiState={{ rightPaneSize }}>
{this.renderHorizontalSplit()}
{rightPaneComponents}
</SplitView>
);
}
}
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const handleColor = theme.palette.blue95;
const paneSpacing = theme.spacing.md;
const resizer = css`
position: relative;
&::before {
content: '';
position: absolute;
transition: 0.2s border-color ease-in-out;
}
&::after {
background: ${theme.colors.panelBorder};
content: '';
position: absolute;
left: 50%;
top: 50%;
transition: 0.2s background ease-in-out;
transform: translate(-50%, -50%);
border-radius: 4px;
}
&:hover {
&::before {
border-color: ${handleColor};
}
&::after {
background: ${handleColor};
}
}
`;
return {
singleLeftPane: css`
height: 100%;
position: absolute;
overflow: hidden;
width: 100%;
`,
resizerV: cx(
resizer,
css`
cursor: col-resize;
width: ${paneSpacing};
&::before {
border-right: 1px solid transparent;
height: 100%;
left: 50%;
transform: translateX(-50%);
}
&::after {
height: 200px;
width: 4px;
}
`
),
resizerH: cx(
resizer,
css`
height: ${paneSpacing};
cursor: row-resize;
margin-left: ${paneSpacing};
&::before {
border-top: 1px solid transparent;
top: 50%;
transform: translateY(-50%);
width: 100%;
}
&::after {
height: 4px;
width: 200px;
}
`
),
};
});