mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 09:05:45 -06:00
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:
parent
02c779cfa3
commit
fee18f143e
@ -67,6 +67,7 @@ class UnthemedDashNavTimeControls extends Component<Props> {
|
||||
onMoveBack = () => {
|
||||
appEvents.emit(CoreEvents.shiftTime, -1);
|
||||
};
|
||||
|
||||
onMoveForward = () => {
|
||||
appEvents.emit(CoreEvents.shiftTime, 1);
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
};
|
||||
}
|
@ -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;
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user