From fee18f143ed12bf74f63804f2ec9405bb09420f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Feb 2020 14:57:16 +0100 Subject: [PATCH] NewPanelEditor: Introduce redux state and reducer (#22070) * New panel editor redux * minor change * Updated * progress * updated * Fixed panel data mutable issue * more actions * Discard works * Updated * Updated --- .../DashNav/DashNavTimeControls.tsx | 1 + .../components/PanelEditor/PanelEditor.tsx | 583 ++++++++---------- .../components/PanelEditor/state/actions.ts | 37 ++ .../components/PanelEditor/state/reducers.ts | 73 +++ .../dashboard/components/PanelEditor/utils.ts | 2 +- .../dashboard/containers/DashboardPage.tsx | 2 +- .../dashboard/state/DashboardModel.ts | 28 +- .../features/dashboard/state/PanelModel.ts | 11 +- .../app/features/dashboard/state/reducers.ts | 2 + public/app/types/store.ts | 2 + 10 files changed, 409 insertions(+), 332 deletions(-) create mode 100644 public/app/features/dashboard/components/PanelEditor/state/actions.ts create mode 100644 public/app/features/dashboard/components/PanelEditor/state/reducers.ts diff --git a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx index 07752b91011..0bdd27b4fa3 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx @@ -67,6 +67,7 @@ class UnthemedDashNavTimeControls extends Component { onMoveBack = () => { appEvents.emit(CoreEvents.shiftTime, -1); }; + onMoveForward = () => { appEvents.emit(CoreEvents.shiftTime, 1); }; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index e28b1d2e3c6..84ee4630075 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -1,14 +1,5 @@ import React, { PureComponent } from 'react'; -import { - GrafanaTheme, - FieldConfigSource, - PanelData, - LoadingState, - DefaultTimeRange, - PanelEvents, - SelectableValue, - TimeRange, -} from '@grafana/data'; +import { GrafanaTheme, FieldConfigSource, PanelData, PanelPlugin, SelectableValue } from '@grafana/data'; import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant, ControlledCollapse } from '@grafana/ui'; import { css, cx } from 'emotion'; import config from 'app/core/config'; @@ -20,17 +11,278 @@ import { DashboardPanel } from '../../dashgrid/DashboardPanel'; import SplitPane from 'react-split-pane'; import { StoreState } from '../../../../types/store'; -import { connect } from 'react-redux'; +import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux'; import { updateLocation } from '../../../../core/reducers/location'; import { Unsubscribable } from 'rxjs'; import { PanelTitle } from './PanelTitle'; import { DisplayMode, displayModes } from './types'; import { PanelEditorTabs } from './PanelEditorTabs'; import { DashNavTimeControls } from '../DashNav/DashNavTimeControls'; -import { LocationState, CoreEvents } from 'app/types'; +import { LocationState } from 'app/types'; import { calculatePanelSize } from './utils'; +import { initPanelEditor, panelEditorCleanUp } from './state/actions'; +import { setDisplayMode, toggleOptionsView, setDiscardChanges } from './state/reducers'; import { FieldConfigEditor } from './FieldConfigEditor'; +interface OwnProps { + dashboard: DashboardModel; + sourcePanel: PanelModel; +} + +interface ConnectedProps { + location: LocationState; + plugin?: PanelPlugin; + panel: PanelModel; + data: PanelData; + mode: DisplayMode; + isPanelOptionsVisible: boolean; + initDone: boolean; +} + +interface DispatchProps { + updateLocation: typeof updateLocation; + initPanelEditor: typeof initPanelEditor; + panelEditorCleanUp: typeof panelEditorCleanUp; + setDisplayMode: typeof setDisplayMode; + toggleOptionsView: typeof toggleOptionsView; + setDiscardChanges: typeof setDiscardChanges; +} + +type Props = OwnProps & ConnectedProps & DispatchProps; + +export class PanelEditorUnconnected extends PureComponent { + querySubscription: Unsubscribable; + + componentDidMount() { + this.props.initPanelEditor(this.props.sourcePanel, this.props.dashboard); + } + + componentWillUnmount() { + this.props.panelEditorCleanUp(); + } + + onPanelExit = () => { + this.props.updateLocation({ + query: { editPanel: null }, + partial: true, + }); + }; + + onDiscard = () => { + this.props.setDiscardChanges(true); + this.props.updateLocation({ + query: { editPanel: null }, + partial: true, + }); + }; + + onFieldConfigsChange = (fieldOptions: FieldConfigSource) => { + // NOTE: for now, assume this is from 'fieldOptions' -- TODO? put on panel model directly? + const { panel } = this.props; + const options = panel.getOptions(); + panel.updateOptions({ + ...options, + fieldOptions, // Assume it is from shared singlestat -- TODO own property? + }); + this.forceUpdate(); + }; + + renderFieldOptions() { + const { plugin, panel, data } = this.props; + + const fieldOptions = panel.options['fieldOptions'] as FieldConfigSource; + + if (!fieldOptions || !plugin) { + return null; + } + + return ( + + ); + } + + onPanelOptionsChanged = (options: any) => { + this.props.panel.updateOptions(options); + this.forceUpdate(); + }; + + /** + * The existing visualization tab + */ + renderVisSettings() { + const { data, panel } = this.props; + const { plugin } = this.props; + + if (!plugin) { + return null; + } + + if (plugin.editor && panel) { + return ( +
+ +
+ ); + } + + return
No editor (angular?)
; + } + + onDragFinished = () => { + document.body.style.cursor = 'auto'; + console.log('TODO, save splitter settings'); + }; + + onPanelTitleChange = (title: string) => { + this.props.panel.title = title; + this.forceUpdate(); + }; + + onDiplayModeChange = (mode: SelectableValue) => { + this.props.setDisplayMode(mode.value); + }; + + onTogglePanelOptions = () => { + this.props.toggleOptionsView(); + }; + + renderHorizontalSplit(styles: any) { + const { dashboard, panel, mode } = this.props; + + return ( + (document.body.style.cursor = 'row-resize')} + onDragFinished={this.onDragFinished} + > +
+ + {({ width, height }) => { + if (width < 3 || height < 3) { + return null; + } + return ( +
+
+ +
+
+ ); + }} +
+
+
+ +
+
+ ); + } + + render() { + const { dashboard, location, panel, mode, isPanelOptionsVisible, initDone } = this.props; + const styles = getStyles(config.theme); + + if (!initDone) { + return null; + } + + return ( +
+
+
+ + +
+
+ v.value === mode)} + options={displayModes} + onChange={this.onDiplayModeChange} + /> + + + Discard + + +
+ +
+
+
+
+ {isPanelOptionsVisible ? ( + (document.body.style.cursor = 'col-resize')} + onDragFinished={this.onDragFinished} + > + {this.renderHorizontalSplit(styles)} +
+ +
+ {this.renderFieldOptions()} + + {this.renderVisSettings()} + +
+
+
+
+ ) : ( + this.renderHorizontalSplit(styles) + )} +
+
+ ); + } +} + +const mapStateToProps: MapStateToProps = (state, props) => ({ + location: state.location, + plugin: state.plugins.panels[props.sourcePanel.type], + panel: state.panelEditorNew.getPanel(), + mode: state.panelEditorNew.mode, + isPanelOptionsVisible: state.panelEditorNew.isPanelOptionsVisible, + data: state.panelEditorNew.getData(), + initDone: state.panelEditorNew.initDone, +}); + +const mapDispatchToProps: MapDispatchToProps = { + updateLocation, + initPanelEditor, + panelEditorCleanUp, + setDisplayMode, + toggleOptionsView, + setDiscardChanges, +}; + +export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEditorUnconnected); + +/* + * Styles + */ const getStyles = stylesFactory((theme: GrafanaTheme) => { const handleColor = selectThemeVariant( { @@ -103,310 +355,3 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { `, }; }); - -interface Props { - dashboard: DashboardModel; - sourcePanel: PanelModel; - updateLocation: typeof updateLocation; - location: LocationState; -} - -interface State { - pluginLoadedCounter: number; - panel: PanelModel; - data: PanelData; - mode: DisplayMode; - showPanelOptions: boolean; -} - -export class PanelEditor extends PureComponent { - querySubscription: Unsubscribable; - - constructor(props: Props) { - super(props); - - // To ensure visualisation settings are re-rendered when plugin has loaded - // panelInitialised event is emmited from PanelChrome - const panel = props.sourcePanel.getEditClone(); - this.state = { - panel, - pluginLoadedCounter: 0, - mode: DisplayMode.Fill, - showPanelOptions: true, - data: { - state: LoadingState.NotStarted, - series: [], - timeRange: DefaultTimeRange, - }, - }; - } - - componentDidMount() { - const { sourcePanel } = this.props; - const { panel } = this.state; - panel.events.on(PanelEvents.panelInitialized, () => { - const { panel } = this.state; - if (panel.angularPanel) { - console.log('Refresh angular panel in new editor'); - } - this.setState(state => ({ - pluginLoadedCounter: state.pluginLoadedCounter + 1, - })); - }); - - // Get data from any pending queries - sourcePanel - .getQueryRunner() - .getData() - .subscribe({ - next: (data: PanelData) => { - this.setState({ data }); - // TODO, cancel???? - }, - }); - - // Listen for queries on the new panel - const queryRunner = panel.getQueryRunner(); - this.querySubscription = queryRunner.getData().subscribe({ - next: (data: PanelData) => this.setState({ data }), - }); - - // Listen to timepicker changes - this.props.dashboard.on(CoreEvents.timeRangeUpdated, this.onTimeRangeUpdated); - } - - componentWillUnmount() { - if (this.querySubscription) { - this.querySubscription.unsubscribe(); - } - //this.cleanUpAngularOptions(); - - // Remove the time listener - this.props.dashboard.off(CoreEvents.timeRangeUpdated, this.onTimeRangeUpdated); - } - - onTimeRangeUpdated = (timeRange: TimeRange) => { - const { panel } = this.state; - if (panel) { - panel.refresh(); - } - }; - - onPanelUpdate = () => { - const { panel } = this.state; - const { dashboard } = this.props; - dashboard.updatePanel(panel); - }; - - onPanelExit = () => { - const { updateLocation } = this.props; - this.onPanelUpdate(); - updateLocation({ - query: { editPanel: null }, - partial: true, - }); - }; - - onDiscard = () => { - this.props.updateLocation({ - query: { editPanel: null }, - partial: true, - }); - }; - - onFieldConfigsChange = (fieldOptions: FieldConfigSource) => { - // NOTE: for now, assume this is from 'fieldOptions' -- TODO? put on panel model directly? - const { panel } = this.state; - const options = panel.getOptions(); - panel.updateOptions({ - ...options, - fieldOptions, // Assume it is from shared singlestat -- TODO own property? - }); - this.forceUpdate(); - }; - - renderFieldOptions() { - const { panel, data } = this.state; - const { plugin } = panel; - const fieldOptions = panel.options['fieldOptions'] as FieldConfigSource; - if (!fieldOptions || !plugin) { - return null; - } - - return ( - - ); - } - - onPanelOptionsChanged = (options: any) => { - this.state.panel.updateOptions(options); - this.forceUpdate(); - }; - - /** - * The existing visualization tab - */ - renderVisSettings() { - const { data, panel } = this.state; - const { plugin } = panel; - if (!plugin) { - return null; // not yet ready - } - - if (plugin.editor && panel) { - return ( -
- -
- ); - } - - return
No editor (angular?)
; - } - - onDragFinished = () => { - document.body.style.cursor = 'auto'; - console.log('TODO, save splitter settings'); - }; - - onPanelTitleChange = (title: string) => { - this.state.panel.title = title; - this.forceUpdate(); - }; - - onDiplayModeChange = (mode: SelectableValue) => { - this.setState({ - mode: mode.value!, - }); - }; - - onTogglePanelOptions = () => { - this.setState({ - showPanelOptions: !this.state.showPanelOptions, - }); - }; - - renderHorizontalSplit(styles: any) { - const { dashboard } = this.props; - const { panel, mode } = this.state; - - return ( - (document.body.style.cursor = 'row-resize')} - onDragFinished={this.onDragFinished} - > -
- - {({ width, height }) => { - if (width < 3 || height < 3) { - return null; - } - return ( -
-
- -
-
- ); - }} -
-
-
- -
-
- ); - } - - render() { - const { dashboard, location } = this.props; - const { panel, mode, showPanelOptions } = this.state; - const styles = getStyles(config.theme); - - if (!panel) { - return null; - } - - return ( -
-
-
- - -
-
- v.value === mode)} - options={displayModes} - onChange={this.onDiplayModeChange} - /> - - - Discard - - -
- -
-
-
-
- {showPanelOptions ? ( - (document.body.style.cursor = 'col-resize')} - onDragFinished={this.onDragFinished} - > - {this.renderHorizontalSplit(styles)} -
- -
- {this.renderFieldOptions()} - - {this.renderVisSettings()} - -
-
-
-
- ) : ( - this.renderHorizontalSplit(styles) - )} -
-
- ); - } -} - -const mapStateToProps = (state: StoreState) => ({ - location: state.location, -}); - -const mapDispatchToProps = { - updateLocation, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(PanelEditor); diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.ts new file mode 100644 index 00000000000..fbb0a6ad774 --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.ts @@ -0,0 +1,37 @@ +import { PanelModel, DashboardModel } from '../../../state'; +import { PanelData } from '@grafana/data'; +import { ThunkResult } from 'app/types'; +import { setEditorPanelData, updateEditorInitState } from './reducers'; + +export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult { + return dispatch => { + const panel = dashboard.initPanelEditor(sourcePanel); + + const queryRunner = panel.getQueryRunner(); + const querySubscription = queryRunner.getData().subscribe({ + next: (data: PanelData) => dispatch(setEditorPanelData(data)), + }); + + dispatch( + updateEditorInitState({ + panel, + sourcePanel, + querySubscription, + }) + ); + }; +} + +export function panelEditorCleanUp(): ThunkResult { + return (dispatch, getStore) => { + const dashboard = getStore().dashboard.getModel(); + const { getPanel, querySubscription, shouldDiscardChanges } = getStore().panelEditorNew; + + if (!shouldDiscardChanges) { + dashboard.updatePanel(getPanel()); + } + + dashboard.exitPanelEditor(); + querySubscription.unsubscribe(); + }; +} diff --git a/public/app/features/dashboard/components/PanelEditor/state/reducers.ts b/public/app/features/dashboard/components/PanelEditor/state/reducers.ts new file mode 100644 index 00000000000..f0a7e8c328f --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/state/reducers.ts @@ -0,0 +1,73 @@ +import { Unsubscribable } from 'rxjs'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { PanelModel } from '../../../state/PanelModel'; +import { PanelData, LoadingState, DefaultTimeRange } from '@grafana/data'; +import { DisplayMode } from '../types'; + +export interface PanelEditorStateNew { + /* These are functions as they are mutaded later on and redux toolkit will Object.freeze state so + * we need to store these using functions instead */ + getSourcePanel: () => PanelModel; + getPanel: () => PanelModel; + getData: () => PanelData; + mode: DisplayMode; + isPanelOptionsVisible: boolean; + querySubscription?: Unsubscribable; + initDone: boolean; + shouldDiscardChanges: boolean; +} + +export const initialState: PanelEditorStateNew = { + getPanel: () => new PanelModel({}), + getSourcePanel: () => new PanelModel({}), + getData: () => ({ + state: LoadingState.NotStarted, + series: [], + timeRange: DefaultTimeRange, + }), + isPanelOptionsVisible: true, + mode: DisplayMode.Fill, + initDone: false, + shouldDiscardChanges: false, +}; + +interface InitEditorPayload { + panel: PanelModel; + sourcePanel: PanelModel; + querySubscription: Unsubscribable; +} + +const pluginsSlice = createSlice({ + name: 'panelEditorNew', + initialState, + reducers: { + updateEditorInitState: (state, action: PayloadAction) => { + state.getPanel = () => action.payload.panel; + state.getSourcePanel = () => action.payload.sourcePanel; + state.querySubscription = action.payload.querySubscription; + state.initDone = true; + }, + setEditorPanelData: (state, action: PayloadAction) => { + state.getData = () => action.payload; + }, + toggleOptionsView: state => { + state.isPanelOptionsVisible = !state.isPanelOptionsVisible; + }, + setDisplayMode: (state, action: PayloadAction) => { + state.mode = action.payload; + }, + setDiscardChanges: (state, action: PayloadAction) => { + state.shouldDiscardChanges = action.payload; + }, + }, +}); + +export const { + updateEditorInitState, + setEditorPanelData, + toggleOptionsView, + setDisplayMode, + setDiscardChanges, +} = pluginsSlice.actions; + +export const panelEditorReducerNew = pluginsSlice.reducer; diff --git a/public/app/features/dashboard/components/PanelEditor/utils.ts b/public/app/features/dashboard/components/PanelEditor/utils.ts index fe36d462fde..636a53558c3 100644 --- a/public/app/features/dashboard/components/PanelEditor/utils.ts +++ b/public/app/features/dashboard/components/PanelEditor/utils.ts @@ -1,7 +1,7 @@ import { CSSProperties } from 'react'; import { PanelModel } from '../../state/PanelModel'; -import { GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, GRID_CELL_HEIGHT } from 'app/core/constants'; import { DisplayMode } from './types'; +import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; export function calculatePanelSize(mode: DisplayMode, width: number, height: number, panel: PanelModel): CSSProperties { if (mode === DisplayMode.Fill) { diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 305cdf865d2..c9706bbf1a4 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -12,7 +12,7 @@ import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { DashNav } from '../components/DashNav'; import { SubMenu } from '../components/SubMenu'; import { DashboardSettings } from '../components/DashboardSettings'; -import PanelEditor from '../components/PanelEditor/PanelEditor'; +import { PanelEditor } from '../components/PanelEditor/PanelEditor'; import { CustomScrollbar, Alert, Portal } from '@grafana/ui'; // Redux diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 56d8bd87a53..eaf5d04f477 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -45,6 +45,7 @@ export class DashboardModel { links: any; gnetId: any; panels: PanelModel[]; + panelInEdit?: PanelModel; // ------------------ // not persisted @@ -62,6 +63,7 @@ export class DashboardModel { templating: true, // needs special handling originalTime: true, originalTemplating: true, + panelInEdit: true, }; constructor(data: any, meta?: DashboardMeta) { @@ -221,6 +223,11 @@ export class DashboardModel { startRefresh() { this.events.emit(PanelEvents.refresh); + if (this.panelInEdit) { + this.panelInEdit.refresh(); + return; + } + for (const panel of this.panels) { if (!this.otherPanelInFullscreen(panel)) { panel.refresh(); @@ -239,15 +246,28 @@ export class DashboardModel { panelInitialized(panel: PanelModel) { panel.initialized(); - // In new panel edit there is no need to trigger refresh as editor retrieves last results from the query runner - // as an initial value - if (!this.otherPanelInFullscreen(panel) && !panel.isNewEdit) { + // refresh new panels unless we are in fullscreen / edit mode + if (!this.otherPanelInFullscreen(panel)) { + panel.refresh(); + } + + // refresh if panel is in edit mode and there is no last result + if (this.panelInEdit === panel && !this.panelInEdit.getQueryRunner().getLastResult()) { panel.refresh(); } } otherPanelInFullscreen(panel: PanelModel) { - return this.meta.fullscreen && !panel.fullscreen; + return (this.meta.fullscreen && !panel.fullscreen) || this.panelInEdit; + } + + initPanelEditor(sourcePanel: PanelModel): PanelModel { + this.panelInEdit = sourcePanel.getEditClone(); + return this.panelInEdit; + } + + exitPanelEditor() { + this.panelInEdit = undefined; } private ensureListExist(data: any) { diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 1015881ab33..30286d3c579 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -38,7 +38,6 @@ const notPersistedProperties: { [str: string]: boolean } = { fullscreen: true, isEditing: true, isInView: true, - isNewEdit: true, hasRefreshed: true, cachedPluginOptions: true, plugin: true, @@ -132,7 +131,6 @@ export class PanelModel { fullscreen: boolean; isEditing: boolean; isInView: boolean; - isNewEdit: boolean; hasRefreshed: boolean; events: Emitter; cacheTimeout?: any; @@ -357,15 +355,14 @@ export class PanelModel { getEditClone() { const clone = new PanelModel(this.getSaveModel()); - clone.queryRunner = new PanelQueryRunner(); + const sourceQueryRunner = this.getQueryRunner(); - // This will send the last result to the new runner - this.getQueryRunner() + // pipe last result to new clone query runner + sourceQueryRunner .getData() .pipe(take(1)) - .subscribe(val => clone.queryRunner.pipeDataToSubject(val)); + .subscribe(val => clone.getQueryRunner().pipeDataToSubject(val)); - clone.isNewEdit = true; return clone; } diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index d4c090603be..dd1544c3e7f 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -8,6 +8,7 @@ import { } from 'app/types'; import { processAclItems } from 'app/core/utils/acl'; import { panelEditorReducer } from '../panel_editor/state/reducers'; +import { panelEditorReducerNew } from '../components/PanelEditor/state/reducers'; import { DashboardModel } from './DashboardModel'; import { PanelModel } from './PanelModel'; @@ -106,4 +107,5 @@ export const dashboardReducer = dashbardSlice.reducer; export default { dashboard: dashboardReducer, panelEditor: panelEditorReducer, + panelEditorNew: panelEditorReducerNew, }; diff --git a/public/app/types/store.ts b/public/app/types/store.ts index 4041422d7ad..54f7d153a3a 100644 --- a/public/app/types/store.ts +++ b/public/app/types/store.ts @@ -16,6 +16,7 @@ import { PluginsState } from './plugins'; import { ApplicationState } from './application'; import { LdapState } from './ldap'; import { PanelEditorState } from '../features/dashboard/panel_editor/state/reducers'; +import { PanelEditorStateNew } from '../features/dashboard/components/PanelEditor/state/reducers'; import { ApiKeysState } from './apiKeys'; export interface StoreState { @@ -27,6 +28,7 @@ export interface StoreState { folder: FolderState; dashboard: DashboardState; panelEditor: PanelEditorState; + panelEditorNew: PanelEditorStateNew; dataSources: DataSourcesState; dataSourceSettings: DataSourceSettingsState; explore: ExploreState;