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
This commit is contained in:
Torkel Ödegaard 2020-02-11 14:57:16 +01:00 committed by GitHub
parent 02c779cfa3
commit fee18f143e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 409 additions and 332 deletions

View File

@ -67,6 +67,7 @@ class UnthemedDashNavTimeControls extends Component<Props> {
onMoveBack = () => {
appEvents.emit(CoreEvents.shiftTime, -1);
};
onMoveForward = () => {
appEvents.emit(CoreEvents.shiftTime, 1);
};

View File

@ -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<Props> {
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 (
<FieldConfigEditor
config={fieldOptions}
custom={plugin.customFieldConfigs}
onChange={this.onFieldConfigsChange}
data={data.series}
/>
);
}
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 (
<div style={{ marginTop: '10px' }}>
<plugin.editor data={data} options={panel.getOptions()} onOptionsChange={this.onPanelOptionsChanged} />
</div>
);
}
return <div>No editor (angular?)</div>;
}
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<DisplayMode>) => {
this.props.setDisplayMode(mode.value);
};
onTogglePanelOptions = () => {
this.props.toggleOptionsView();
};
renderHorizontalSplit(styles: any) {
const { dashboard, panel, mode } = this.props;
return (
<SplitPane
split="horizontal"
minSize={50}
primary="second"
defaultSize="40%"
resizerClassName={styles.resizerH}
onDragStarted={() => (document.body.style.cursor = 'row-resize')}
onDragFinished={this.onDragFinished}
>
<div className={styles.panelWrapper}>
<AutoSizer>
{({ width, height }) => {
if (width < 3 || height < 3) {
return null;
}
return (
<div className={styles.centeringContainer} style={{ width, height }}>
<div style={calculatePanelSize(mode, width, height, panel)}>
<DashboardPanel
dashboard={dashboard}
panel={panel}
isEditing={false}
isInEditMode
isFullscreen={false}
isInView={true}
/>
</div>
</div>
);
}}
</AutoSizer>
</div>
<div className={styles.noScrollPaneContent}>
<PanelEditorTabs panel={panel} dashboard={dashboard} />
</div>
</SplitPane>
);
}
render() {
const { dashboard, location, panel, mode, isPanelOptionsVisible, initDone } = this.props;
const styles = getStyles(config.theme);
if (!initDone) {
return null;
}
return (
<div className={styles.wrapper}>
<div className={styles.toolbar}>
<div className={styles.toolbarLeft}>
<button className="navbar-edit__back-btn" onClick={this.onPanelExit}>
<i className="fa fa-arrow-left"></i>
</button>
<PanelTitle value={panel.title} onChange={this.onPanelTitleChange} />
</div>
<div className={styles.toolbarLeft}>
<Forms.Select
value={displayModes.find(v => v.value === mode)}
options={displayModes}
onChange={this.onDiplayModeChange}
/>
<Forms.Button icon="fa fa-cog" variant="secondary" onClick={this.onTogglePanelOptions} />
<Forms.Button variant="destructive" onClick={this.onDiscard}>
Discard
</Forms.Button>
<div>
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
</div>
</div>
</div>
<div className={styles.panes}>
{isPanelOptionsVisible ? (
<SplitPane
split="vertical"
minSize={100}
primary="second"
defaultSize={350}
resizerClassName={styles.resizerV}
onDragStarted={() => (document.body.style.cursor = 'col-resize')}
onDragFinished={this.onDragFinished}
>
{this.renderHorizontalSplit(styles)}
<div className={styles.noScrollPaneContent}>
<CustomScrollbar>
<div style={{ padding: '10px' }}>
{this.renderFieldOptions()}
<ControlledCollapse label="Visualization Settings" collapsible>
{this.renderVisSettings()}
</ControlledCollapse>
</div>
</CustomScrollbar>
</div>
</SplitPane>
) : (
this.renderHorizontalSplit(styles)
)}
</div>
</div>
);
}
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (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<DispatchProps, OwnProps> = {
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<Props, State> {
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 (
<FieldConfigEditor
config={fieldOptions}
custom={plugin.customFieldConfigs}
onChange={this.onFieldConfigsChange}
data={data.series}
/>
);
}
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 (
<div style={{ marginTop: '10px' }}>
<plugin.editor data={data} options={panel.getOptions()} onOptionsChange={this.onPanelOptionsChanged} />
</div>
);
}
return <div>No editor (angular?)</div>;
}
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<DisplayMode>) => {
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 (
<SplitPane
split="horizontal"
minSize={50}
primary="second"
defaultSize="40%"
resizerClassName={styles.resizerH}
onDragStarted={() => (document.body.style.cursor = 'row-resize')}
onDragFinished={this.onDragFinished}
>
<div className={styles.panelWrapper}>
<AutoSizer>
{({ width, height }) => {
if (width < 3 || height < 3) {
return null;
}
return (
<div className={styles.centeringContainer} style={{ width, height }}>
<div style={calculatePanelSize(mode, width, height, panel)}>
<DashboardPanel
dashboard={dashboard}
panel={panel}
isEditing={false}
isInEditMode
isFullscreen={false}
isInView={true}
/>
</div>
</div>
);
}}
</AutoSizer>
</div>
<div className={styles.noScrollPaneContent}>
<PanelEditorTabs panel={panel} dashboard={dashboard} />
</div>
</SplitPane>
);
}
render() {
const { dashboard, location } = this.props;
const { panel, mode, showPanelOptions } = this.state;
const styles = getStyles(config.theme);
if (!panel) {
return null;
}
return (
<div className={styles.wrapper}>
<div className={styles.toolbar}>
<div className={styles.toolbarLeft}>
<button className="navbar-edit__back-btn" onClick={this.onPanelExit}>
<i className="fa fa-arrow-left"></i>
</button>
<PanelTitle value={panel.title} onChange={this.onPanelTitleChange} />
</div>
<div className={styles.toolbarLeft}>
<Forms.Select
value={displayModes.find(v => v.value === mode)}
options={displayModes}
onChange={this.onDiplayModeChange}
/>
<Forms.Button icon="fa fa-cog" variant="secondary" onClick={this.onTogglePanelOptions} />
<Forms.Button variant="destructive" onClick={this.onDiscard}>
Discard
</Forms.Button>
<div>
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
</div>
</div>
</div>
<div className={styles.panes}>
{showPanelOptions ? (
<SplitPane
split="vertical"
minSize={100}
primary="second"
defaultSize={350}
resizerClassName={styles.resizerV}
onDragStarted={() => (document.body.style.cursor = 'col-resize')}
onDragFinished={this.onDragFinished}
>
{this.renderHorizontalSplit(styles)}
<div className={styles.noScrollPaneContent}>
<CustomScrollbar>
<div style={{ padding: '10px' }}>
{this.renderFieldOptions()}
<ControlledCollapse label="Visualization Settings" collapsible>
{this.renderVisSettings()}
</ControlledCollapse>
</div>
</CustomScrollbar>
</div>
</SplitPane>
) : (
this.renderHorizontalSplit(styles)
)}
</div>
</div>
);
}
}
const mapStateToProps = (state: StoreState) => ({
location: state.location,
});
const mapDispatchToProps = {
updateLocation,
};
export default connect(mapStateToProps, mapDispatchToProps)(PanelEditor);

View File

@ -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<void> {
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<void> {
return (dispatch, getStore) => {
const dashboard = getStore().dashboard.getModel();
const { getPanel, querySubscription, shouldDiscardChanges } = getStore().panelEditorNew;
if (!shouldDiscardChanges) {
dashboard.updatePanel(getPanel());
}
dashboard.exitPanelEditor();
querySubscription.unsubscribe();
};
}

View File

@ -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<InitEditorPayload>) => {
state.getPanel = () => action.payload.panel;
state.getSourcePanel = () => action.payload.sourcePanel;
state.querySubscription = action.payload.querySubscription;
state.initDone = true;
},
setEditorPanelData: (state, action: PayloadAction<PanelData>) => {
state.getData = () => action.payload;
},
toggleOptionsView: state => {
state.isPanelOptionsVisible = !state.isPanelOptionsVisible;
},
setDisplayMode: (state, action: PayloadAction<DisplayMode>) => {
state.mode = action.payload;
},
setDiscardChanges: (state, action: PayloadAction<boolean>) => {
state.shouldDiscardChanges = action.payload;
},
},
});
export const {
updateEditorInitState,
setEditorPanelData,
toggleOptionsView,
setDisplayMode,
setDiscardChanges,
} = pluginsSlice.actions;
export const panelEditorReducerNew = pluginsSlice.reducer;

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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;