Core: Move SplitPane layout from PanelEdit. (#29266)

* move split pane logic to its own component

* fix update function typing

* destruct updateuistate

* updates after review

* reword the rightpane to work for text panel
This commit is contained in:
Peter Holmberg 2020-11-25 11:24:06 +01:00 committed by GitHub
parent 5773929953
commit 0b2a6ec7fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 213 additions and 153 deletions

View File

@ -0,0 +1,177 @@
import React, { createRef, MutableRefObject, PureComponent, ReactNode } from 'react';
import SplitPane from 'react-split-pane';
import { css, cx } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config';
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 = createRef<number>();
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 as MutableRefObject<number>).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 as number) : (uiState.topPaneSize as number) * 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.
*/
const maxHeight = window.innerHeight - 120;
if (Array.isArray(leftPaneComponents)) {
return (
<SplitPane
split="horizontal"
maxSize={maxHeight}
primary="first"
size={topPaneSize < 200 ? 200 : topPaneSize}
pane2Style={{ minHeight: 0 }}
resizerClassName={styles.resizerH}
onDragStarted={this.onDragStarted}
onDragFinished={size => this.onDragFinished(Pane.Top, size)}
>
{leftPaneComponents}
</SplitPane>
);
}
return leftPaneComponents;
}
render() {
const { rightPaneVisible, rightPaneComponents, uiState } = this.props;
// Limit options pane width to 90% of screen.
const maxWidth = window.innerWidth * 0.9;
const styles = getStyles(config.theme);
// Need to handle when width is relative. ie a percentage of the viewport
const rightPaneSize =
uiState.rightPaneSize <= 1
? (uiState.rightPaneSize as number) * window.innerWidth
: (uiState.rightPaneSize as number);
if (!rightPaneVisible) {
return this.renderHorizontalSplit();
}
return (
<SplitPane
split="vertical"
maxSize={maxWidth}
size={rightPaneSize >= 300 ? rightPaneSize : 300}
primary="second"
resizerClassName={styles.resizerV}
onDragStarted={() => (document.body.style.cursor = 'col-resize')}
onDragFinished={size => this.onDragFinished(Pane.Right, size)}
>
{this.renderHorizontalSplit()}
{rightPaneComponents}
</SplitPane>
);
}
}
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const handleColor = theme.palette.blue95;
const paneSpacing = theme.spacing.md;
const resizer = css`
font-style: italic;
background: transparent;
border-top: 0;
border-right: 0;
border-bottom: 0;
border-left: 0;
border-color: transparent;
border-style: solid;
transition: 0.2s border-color ease-in-out;
&:hover {
border-color: ${handleColor};
}
`;
return {
resizerV: cx(
resizer,
css`
cursor: col-resize;
width: ${paneSpacing};
border-right-width: 1px;
margin-top: 18px;
`
),
resizerH: cx(
resizer,
css`
height: ${paneSpacing};
cursor: row-resize;
position: relative;
top: 0px;
z-index: 1;
border-top-width: 1px;
margin-left: ${paneSpacing};
`
),
};
});

View File

@ -1,7 +1,6 @@
import React, { createRef, MutableRefObject, PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import SplitPane from 'react-split-pane';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { Unsubscribable } from 'rxjs'; import { Unsubscribable } from 'rxjs';
@ -19,6 +18,7 @@ import { OptionsPaneContent } from './OptionsPaneContent';
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton'; import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems'; import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems';
import { BackButton } from 'app/core/components/BackButton/BackButton'; import { BackButton } from 'app/core/components/BackButton/BackButton';
import { SplitPaneWrapper } from 'app/core/components/ThreePaneSplit/SplitPaneWrapper';
import { SaveDashboardModalProxy } from '../SaveDashboard/SaveDashboardModalProxy'; import { SaveDashboardModalProxy } from '../SaveDashboard/SaveDashboardModalProxy';
import { DashboardPanel } from '../../dashgrid/DashboardPanel'; import { DashboardPanel } from '../../dashgrid/DashboardPanel';
@ -65,28 +65,14 @@ type Props = OwnProps & ConnectedProps & DispatchProps;
export class PanelEditorUnconnected extends PureComponent<Props> { export class PanelEditorUnconnected extends PureComponent<Props> {
querySubscription: Unsubscribable; querySubscription: Unsubscribable;
rafToken = createRef<number>();
componentDidMount() { componentDidMount() {
this.props.initPanelEditor(this.props.sourcePanel, this.props.dashboard); this.props.initPanelEditor(this.props.sourcePanel, this.props.dashboard);
window.addEventListener('resize', this.updateSplitPaneSize);
} }
componentWillUnmount() { componentWillUnmount() {
this.props.panelEditorCleanUp(); this.props.panelEditorCleanUp();
window.removeEventListener('resize', this.updateSplitPaneSize);
} }
updateSplitPaneSize = () => {
if (this.rafToken.current !== undefined) {
window.cancelAnimationFrame(this.rafToken.current!);
}
(this.rafToken as MutableRefObject<number>).current = window.requestAnimationFrame(() => {
this.forceUpdate();
});
};
onPanelExit = () => { onPanelExit = () => {
this.props.updateLocation({ this.props.updateLocation({
query: { editPanel: null, tab: null }, query: { editPanel: null, tab: null },
@ -138,30 +124,6 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
this.forceUpdate(); 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 { updatePanelEditorUIState } = this.props;
if (pane === Pane.Top) {
updatePanelEditorUIState({
topPaneSize: size / window.innerHeight,
});
} else {
updatePanelEditorUIState({
rightPaneSize: size / window.innerWidth,
});
}
};
onDragStarted = () => {
document.body.style.cursor = 'row-resize';
};
onDisplayModeChange = (mode: DisplayMode) => { onDisplayModeChange = (mode: DisplayMode) => {
const { updatePanelEditorUIState } = this.props; const { updatePanelEditorUIState } = this.props;
updatePanelEditorUIState({ updatePanelEditorUIState({
@ -177,7 +139,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
renderPanel = (styles: EditorStyles) => { renderPanel = (styles: EditorStyles) => {
const { dashboard, panel, tabs, uiState } = this.props; const { dashboard, panel, tabs, uiState } = this.props;
return ( return (
<div className={cx(styles.mainPaneWrapper, tabs.length === 0 && styles.mainPaneWrapperNoTabs)}> <div className={cx(styles.mainPaneWrapper, tabs.length === 0 && styles.mainPaneWrapperNoTabs)} key="panel">
{this.renderPanelToolbar(styles)} {this.renderPanelToolbar(styles)}
<div className={styles.panelWrapper}> <div className={styles.panelWrapper}>
<AutoSizer> <AutoSizer>
@ -205,38 +167,22 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
); );
}; };
renderHorizontalSplit(styles: EditorStyles) { renderPanelAndEditor(styles: EditorStyles) {
const { dashboard, panel, tabs, uiState } = this.props; const { panel, dashboard, tabs } = this.props;
/*
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.
*/
const windowHeight = window.innerHeight - 120;
const size = uiState.topPaneSize >= 1 ? uiState.topPaneSize : (uiState.topPaneSize as number) * window.innerHeight;
return tabs.length > 0 ? ( if (tabs.length > 0) {
<SplitPane return [
split="horizontal" this.renderPanel(styles),
minSize={200} <div
maxSize={windowHeight} className={styles.tabsWrapper}
primary="first" aria-label={selectors.components.PanelEditor.DataPane.content}
size={size} key="panel-editor-tabs"
/* Use persisted state for default size */ >
defaultSize={uiState.topPaneSize}
pane2Style={{ minHeight: 0 }}
resizerClassName={styles.resizerH}
onDragStarted={this.onDragStarted}
onDragFinished={size => this.onDragFinished(Pane.Top, size)}
>
{this.renderPanel(styles)}
<div className={styles.tabsWrapper} aria-label={selectors.components.PanelEditor.DataPane.content}>
<PanelEditorTabs panel={panel} dashboard={dashboard} tabs={tabs} onChangeTab={this.onChangeTab} /> <PanelEditorTabs panel={panel} dashboard={dashboard} tabs={tabs} onChangeTab={this.onChangeTab} />
</div> </div>,
</SplitPane> ];
) : ( }
this.renderPanel(styles) return this.renderPanel(styles);
);
} }
renderTemplateVariables(styles: EditorStyles) { renderTemplateVariables(styles: EditorStyles) {
@ -319,8 +265,13 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
); );
} }
renderOptionsPane(width: number) { renderOptionsPane() {
const { plugin, dashboard, panel } = this.props; const { plugin, dashboard, panel, uiState } = this.props;
const rightPaneSize =
uiState.rightPaneSize <= 1
? (uiState.rightPaneSize as number) * window.innerWidth
: (uiState.rightPaneSize as number);
if (!plugin) { if (!plugin) {
return <div />; return <div />;
@ -331,7 +282,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
plugin={plugin} plugin={plugin}
dashboard={dashboard} dashboard={dashboard}
panel={panel} panel={panel}
width={width} width={rightPaneSize}
onClose={this.onTogglePanelOptions} onClose={this.onTogglePanelOptions}
onFieldConfigsChange={this.onFieldConfigChange} onFieldConfigsChange={this.onFieldConfigChange}
onPanelOptionsChanged={this.onPanelOptionsChanged} onPanelOptionsChanged={this.onPanelOptionsChanged}
@ -340,39 +291,8 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
); );
} }
renderWithOptionsPane(styles: EditorStyles) {
const { uiState } = this.props;
// Limit options pane width to 90% of screen.
const maxWidth = window.innerWidth * 0.9;
// Need to handle when width is relative. ie a percentage of the viewport
const width =
uiState.rightPaneSize <= 1
? (uiState.rightPaneSize as number) * window.innerWidth
: (uiState.rightPaneSize as number);
return (
<SplitPane
split="vertical"
minSize={300}
maxSize={maxWidth}
size={width >= 300 ? width : 300}
primary="second"
/* Use persisted state for default size */
defaultSize={uiState.rightPaneSize}
resizerClassName={styles.resizerV}
onDragStarted={() => (document.body.style.cursor = 'col-resize')}
onDragFinished={size => this.onDragFinished(Pane.Right, size)}
>
{this.renderHorizontalSplit(styles)}
{this.renderOptionsPane(width)}
</SplitPane>
);
}
render() { render() {
const { initDone, uiState } = this.props; const { initDone, updatePanelEditorUIState, uiState } = this.props;
const styles = getStyles(config.theme, this.props); const styles = getStyles(config.theme, this.props);
if (!initDone) { if (!initDone) {
@ -383,7 +303,13 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
<div className={styles.wrapper} aria-label={selectors.components.PanelEditor.General.content}> <div className={styles.wrapper} aria-label={selectors.components.PanelEditor.General.content}>
{this.editorToolbar(styles)} {this.editorToolbar(styles)}
<div className={styles.verticalSplitPanesWrapper}> <div className={styles.verticalSplitPanesWrapper}>
{uiState.isPanelOptionsVisible ? this.renderWithOptionsPane(styles) : this.renderHorizontalSplit(styles)} <SplitPaneWrapper
leftPaneComponents={this.renderPanelAndEditor(styles)}
rightPaneComponents={this.renderOptionsPane()}
uiState={uiState}
updateUiState={updatePanelEditorUIState}
rightPaneVisible={uiState.isPanelOptionsVisible}
/>
</div> </div>
</div> </div>
); );
@ -416,35 +342,13 @@ const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEditorUnconnected); export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEditorUnconnected);
enum Pane {
Right,
Top,
}
/* /*
* Styles * Styles
*/ */
export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => { export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
const { uiState } = props; const { uiState } = props;
const handleColor = theme.palette.blue95;
const paneSpacing = theme.spacing.md; const paneSpacing = theme.spacing.md;
const resizer = css`
font-style: italic;
background: transparent;
border-top: 0;
border-right: 0;
border-bottom: 0;
border-left: 0;
border-color: transparent;
border-style: solid;
transition: 0.2s border-color ease-in-out;
&:hover {
border-color: ${handleColor};
}
`;
return { return {
wrapper: css` wrapper: css`
width: 100%; width: 100%;
@ -488,27 +392,6 @@ export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
width: 100%; width: 100%;
padding-left: ${paneSpacing}; padding-left: ${paneSpacing};
`, `,
resizerV: cx(
resizer,
css`
cursor: col-resize;
width: ${paneSpacing};
border-right-width: 1px;
margin-top: 18px;
`
),
resizerH: cx(
resizer,
css`
height: ${paneSpacing};
cursor: row-resize;
position: relative;
top: 0px;
z-index: 1;
border-top-width: 1px;
margin-left: ${paneSpacing};
`
),
tabsWrapper: css` tabsWrapper: css`
height: 100%; height: 100%;
width: 100%; width: 100%;

View File

@ -8,7 +8,7 @@ import {
updateEditorInitState, updateEditorInitState,
} from './reducers'; } from './reducers';
import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers'; import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers';
import store from '../../../../../core/store'; import store from 'app/core/store';
export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult<void> { export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult<void> {
return dispatch => { return dispatch => {

View File

@ -17,9 +17,9 @@ export interface PanelEditorUIState {
/* Visualization options pane visibility */ /* Visualization options pane visibility */
isPanelOptionsVisible: boolean; isPanelOptionsVisible: boolean;
/* Pixels or percentage */ /* Pixels or percentage */
rightPaneSize: number | string; rightPaneSize: number;
/* Pixels or percentage */ /* Pixels or percentage */
topPaneSize: number | string; topPaneSize: number;
/* Visualization size mode */ /* Visualization size mode */
mode: DisplayMode; mode: DisplayMode;
} }