mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
New panel editor: Persist panel editor ui state (#22210)
This commit is contained in:
parent
21b53b7d79
commit
131a3248ba
@ -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);
|
||||||
|
@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user