New panel editor: Persist panel editor ui state (#22210)

This commit is contained in:
Dominik Prokop 2020-02-15 20:31:11 +01:00 committed by GitHub
parent 21b53b7d79
commit 131a3248ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 82 additions and 38 deletions

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { GrafanaTheme, FieldConfigSource, PanelData, PanelPlugin, SelectableValue } from '@grafana/data'; import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant, Icon } from '@grafana/ui'; import { CustomScrollbar, Forms, Icon, selectThemeVariant, stylesFactory } from '@grafana/ui';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import config from 'app/core/config'; import config from 'app/core/config';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
@ -11,7 +11,7 @@ import { DashboardPanel } from '../../dashgrid/DashboardPanel';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
import { StoreState } from '../../../../types/store'; import { StoreState } from '../../../../types/store';
import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux'; import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { updateLocation } from '../../../../core/reducers/location'; import { updateLocation } from '../../../../core/reducers/location';
import { Unsubscribable } from 'rxjs'; import { Unsubscribable } from 'rxjs';
import { PanelTitle } from './PanelTitle'; import { PanelTitle } from './PanelTitle';
@ -20,13 +20,18 @@ import { PanelEditorTabs } from './PanelEditorTabs';
import { DashNavTimeControls } from '../DashNav/DashNavTimeControls'; import { DashNavTimeControls } from '../DashNav/DashNavTimeControls';
import { LocationState } from 'app/types'; import { LocationState } from 'app/types';
import { calculatePanelSize } from './utils'; import { calculatePanelSize } from './utils';
import { initPanelEditor, panelEditorCleanUp } from './state/actions'; import { initPanelEditor, panelEditorCleanUp, updatePanelEditorUIState } from './state/actions';
import { setDisplayMode, toggleOptionsView, setDiscardChanges } from './state/reducers'; import { PanelEditorUIState, setDiscardChanges } from './state/reducers';
import { FieldConfigEditor } from './FieldConfigEditor'; import { FieldConfigEditor } from './FieldConfigEditor';
import { OptionsGroup } from './OptionsGroup'; import { OptionsGroup } from './OptionsGroup';
import { getPanelEditorTabs } from './state/selectors'; import { getPanelEditorTabs } from './state/selectors';
import { getPanelStateById } from '../../state/selectors'; import { getPanelStateById } from '../../state/selectors';
enum Pane {
Right,
Top,
}
interface OwnProps { interface OwnProps {
dashboard: DashboardModel; dashboard: DashboardModel;
sourcePanel: PanelModel; sourcePanel: PanelModel;
@ -37,19 +42,17 @@ interface ConnectedProps {
plugin?: PanelPlugin; plugin?: PanelPlugin;
panel: PanelModel; panel: PanelModel;
data: PanelData; data: PanelData;
mode: DisplayMode;
isPanelOptionsVisible: boolean;
initDone: boolean; initDone: boolean;
tabs: PanelEditorTab[]; tabs: PanelEditorTab[];
uiState: PanelEditorUIState;
} }
interface DispatchProps { interface DispatchProps {
updateLocation: typeof updateLocation; updateLocation: typeof updateLocation;
initPanelEditor: typeof initPanelEditor; initPanelEditor: typeof initPanelEditor;
panelEditorCleanUp: typeof panelEditorCleanUp; panelEditorCleanUp: typeof panelEditorCleanUp;
setDisplayMode: typeof setDisplayMode;
toggleOptionsView: typeof toggleOptionsView;
setDiscardChanges: typeof setDiscardChanges; setDiscardChanges: typeof setDiscardChanges;
updatePanelEditorUIState: typeof updatePanelEditorUIState;
} }
type Props = OwnProps & ConnectedProps & DispatchProps; type Props = OwnProps & ConnectedProps & DispatchProps;
@ -141,8 +144,13 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
return <div>No editor (angular?)</div>; return <div>No editor (angular?)</div>;
} }
onDragFinished = () => { onDragFinished = (pane: Pane, size: number) => {
document.body.style.cursor = 'auto'; document.body.style.cursor = 'auto';
const targetPane = pane === Pane.Top ? 'topPaneSize' : 'rightPaneSize';
const { updatePanelEditorUIState } = this.props;
updatePanelEditorUIState({
[targetPane]: size,
});
}; };
onDragStarted = () => { onDragStarted = () => {
@ -155,26 +163,31 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
}; };
onDiplayModeChange = (mode: SelectableValue<DisplayMode>) => { onDiplayModeChange = (mode: SelectableValue<DisplayMode>) => {
this.props.setDisplayMode(mode.value); const { updatePanelEditorUIState } = this.props;
updatePanelEditorUIState({
mode: mode.value,
});
}; };
onTogglePanelOptions = () => { onTogglePanelOptions = () => {
this.props.toggleOptionsView(); const { uiState, updatePanelEditorUIState } = this.props;
updatePanelEditorUIState({ isPanelOptionsVisible: !uiState.isPanelOptionsVisible });
}; };
renderHorizontalSplit(styles: any) { renderHorizontalSplit(styles: any) {
const { dashboard, panel, mode, tabs, data } = this.props; const { dashboard, panel, tabs, data, uiState } = this.props;
return ( return (
<SplitPane <SplitPane
split="horizontal" split="horizontal"
minSize={50} minSize={50}
primary="first" primary="first"
defaultSize="45%" /* Use persisted state for default size */
defaultSize={uiState.topPaneSize}
pane2Style={{ minHeight: 0 }} pane2Style={{ minHeight: 0 }}
resizerClassName={styles.resizerH} resizerClassName={styles.resizerH}
onDragStarted={this.onDragStarted} onDragStarted={this.onDragStarted}
onDragFinished={this.onDragFinished} onDragFinished={size => this.onDragFinished(Pane.Top, size)}
> >
<div className={styles.panelWrapper}> <div className={styles.panelWrapper}>
<AutoSizer> <AutoSizer>
@ -184,7 +197,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
} }
return ( return (
<div className={styles.centeringContainer} style={{ width, height }}> <div className={styles.centeringContainer} style={{ width, height }}>
<div style={calculatePanelSize(mode, width, height, panel)}> <div style={calculatePanelSize(uiState.mode, width, height, panel)}>
<DashboardPanel <DashboardPanel
dashboard={dashboard} dashboard={dashboard}
panel={panel} panel={panel}
@ -207,7 +220,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
} }
render() { render() {
const { dashboard, location, panel, mode, isPanelOptionsVisible, initDone } = this.props; const { dashboard, location, panel, uiState, initDone } = this.props;
const styles = getStyles(config.theme); const styles = getStyles(config.theme);
if (!initDone) { if (!initDone) {
@ -234,7 +247,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
</div> </div>
<div className={styles.toolbarItem}> <div className={styles.toolbarItem}>
<Forms.Select <Forms.Select
value={displayModes.find(v => v.value === mode)} value={displayModes.find(v => v.value === uiState.mode)}
options={displayModes} options={displayModes}
onChange={this.onDiplayModeChange} onChange={this.onDiplayModeChange}
/> />
@ -253,15 +266,16 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
</div> </div>
</div> </div>
<div className={styles.editorBody}> <div className={styles.editorBody}>
{isPanelOptionsVisible ? ( {uiState.isPanelOptionsVisible ? (
<SplitPane <SplitPane
split="vertical" split="vertical"
minSize={100} minSize={100}
primary="second" primary="second"
defaultSize={350} /* Use persisted state for default size */
defaultSize={uiState.rightPaneSize}
resizerClassName={styles.resizerV} resizerClassName={styles.resizerV}
onDragStarted={() => (document.body.style.cursor = 'col-resize')} onDragStarted={() => (document.body.style.cursor = 'col-resize')}
onDragFinished={this.onDragFinished} onDragFinished={size => this.onDragFinished(Pane.Right, size)}
> >
{this.renderHorizontalSplit(styles)} {this.renderHorizontalSplit(styles)}
<div className={styles.panelOptionsPane}> <div className={styles.panelOptionsPane}>
@ -292,11 +306,10 @@ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (
location: state.location, location: state.location,
plugin: plugin, plugin: plugin,
panel: state.panelEditorNew.getPanel(), panel: state.panelEditorNew.getPanel(),
mode: state.panelEditorNew.mode,
isPanelOptionsVisible: state.panelEditorNew.isPanelOptionsVisible,
data: state.panelEditorNew.getData(), data: state.panelEditorNew.getData(),
initDone: state.panelEditorNew.initDone, initDone: state.panelEditorNew.initDone,
tabs: getPanelEditorTabs(state.location, plugin), tabs: getPanelEditorTabs(state.location, plugin),
uiState: state.panelEditorNew.ui,
}; };
}; };
@ -304,9 +317,8 @@ const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
updateLocation, updateLocation,
initPanelEditor, initPanelEditor,
panelEditorCleanUp, panelEditorCleanUp,
setDisplayMode,
toggleOptionsView,
setDiscardChanges, setDiscardChanges,
updatePanelEditorUIState,
}; };
export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEditorUnconnected); export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEditorUnconnected);

View File

@ -1,7 +1,15 @@
import { PanelModel, DashboardModel } from '../../../state'; import { PanelModel, DashboardModel } from '../../../state';
import { PanelData } from '@grafana/data'; import { PanelData } from '@grafana/data';
import { ThunkResult } from 'app/types'; import { ThunkResult } from 'app/types';
import { setEditorPanelData, updateEditorInitState, closeCompleted } from './reducers'; import {
setEditorPanelData,
updateEditorInitState,
closeCompleted,
PanelEditorUIState,
setPanelEditorUIState,
PANEL_EDITOR_UI_STATE_STORAGE_KEY,
} from './reducers';
import store from '../../../../../core/store';
export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult<void> { export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult<void> {
return dispatch => { return dispatch => {
@ -45,3 +53,11 @@ export function panelEditorCleanUp(): ThunkResult<void> {
dispatch(closeCompleted()); dispatch(closeCompleted());
}; };
} }
export function updatePanelEditorUIState(uiState: Partial<PanelEditorUIState>): ThunkResult<void> {
return (dispatch, getStore) => {
const nextState = { ...getStore().panelEditorNew.ui, ...uiState };
dispatch(setPanelEditorUIState(nextState));
store.setObject(PANEL_EDITOR_UI_STATE_STORAGE_KEY, nextState);
};
}

View File

@ -3,6 +3,25 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { PanelModel } from '../../../state/PanelModel'; import { PanelModel } from '../../../state/PanelModel';
import { PanelData, LoadingState, DefaultTimeRange } from '@grafana/data'; import { PanelData, LoadingState, DefaultTimeRange } from '@grafana/data';
import { DisplayMode } from '../types'; import { DisplayMode } from '../types';
import store from '../../../../../core/store';
export const PANEL_EDITOR_UI_STATE_STORAGE_KEY = 'grafana.dashboard.editor.ui';
export const DEFAULT_PANEL_EDITOR_UI_STATE: PanelEditorUIState = {
isPanelOptionsVisible: true,
rightPaneSize: 350,
topPaneSize: '45%',
mode: DisplayMode.Fill,
};
export interface PanelEditorUIState {
isPanelOptionsVisible: boolean;
// annotating as number or string since size can be expressed as static value or percentage
rightPaneSize: number | string;
// annotating as number or string since size can be expressed as static value or percentage
topPaneSize: number | string;
mode: DisplayMode;
}
export interface PanelEditorStateNew { export interface PanelEditorStateNew {
/* These are functions as they are mutaded later on and redux toolkit will Object.freeze state so /* These are functions as they are mutaded later on and redux toolkit will Object.freeze state so
@ -10,12 +29,11 @@ export interface PanelEditorStateNew {
getSourcePanel: () => PanelModel; getSourcePanel: () => PanelModel;
getPanel: () => PanelModel; getPanel: () => PanelModel;
getData: () => PanelData; getData: () => PanelData;
mode: DisplayMode;
isPanelOptionsVisible: boolean;
querySubscription?: Unsubscribable; querySubscription?: Unsubscribable;
initDone: boolean; initDone: boolean;
shouldDiscardChanges: boolean; shouldDiscardChanges: boolean;
isOpen: boolean; isOpen: boolean;
ui: PanelEditorUIState;
} }
export const initialState: PanelEditorStateNew = { export const initialState: PanelEditorStateNew = {
@ -26,11 +44,13 @@ export const initialState: PanelEditorStateNew = {
series: [], series: [],
timeRange: DefaultTimeRange, timeRange: DefaultTimeRange,
}), }),
isPanelOptionsVisible: true,
mode: DisplayMode.Fill,
initDone: false, initDone: false,
shouldDiscardChanges: false, shouldDiscardChanges: false,
isOpen: false, isOpen: false,
ui: {
...DEFAULT_PANEL_EDITOR_UI_STATE,
...store.getObject(PANEL_EDITOR_UI_STATE_STORAGE_KEY, DEFAULT_PANEL_EDITOR_UI_STATE),
},
}; };
interface InitEditorPayload { interface InitEditorPayload {
@ -53,15 +73,12 @@ const pluginsSlice = createSlice({
setEditorPanelData: (state, action: PayloadAction<PanelData>) => { setEditorPanelData: (state, action: PayloadAction<PanelData>) => {
state.getData = () => action.payload; state.getData = () => action.payload;
}, },
toggleOptionsView: state => {
state.isPanelOptionsVisible = !state.isPanelOptionsVisible;
},
setDisplayMode: (state, action: PayloadAction<DisplayMode>) => {
state.mode = action.payload;
},
setDiscardChanges: (state, action: PayloadAction<boolean>) => { setDiscardChanges: (state, action: PayloadAction<boolean>) => {
state.shouldDiscardChanges = action.payload; state.shouldDiscardChanges = action.payload;
}, },
setPanelEditorUIState: (state, action: PayloadAction<Partial<PanelEditorUIState>>) => {
state.ui = { ...state.ui, ...action.payload };
},
closeCompleted: state => { closeCompleted: state => {
state.isOpen = false; state.isOpen = false;
state.initDone = false; state.initDone = false;
@ -72,10 +89,9 @@ const pluginsSlice = createSlice({
export const { export const {
updateEditorInitState, updateEditorInitState,
setEditorPanelData, setEditorPanelData,
toggleOptionsView,
setDisplayMode,
setDiscardChanges, setDiscardChanges,
closeCompleted, closeCompleted,
setPanelEditorUIState,
} = pluginsSlice.actions; } = pluginsSlice.actions;
export const panelEditorReducerNew = pluginsSlice.reducer; export const panelEditorReducerNew = pluginsSlice.reducer;