mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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 = () => {
|
onMoveBack = () => {
|
||||||
appEvents.emit(CoreEvents.shiftTime, -1);
|
appEvents.emit(CoreEvents.shiftTime, -1);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMoveForward = () => {
|
onMoveForward = () => {
|
||||||
appEvents.emit(CoreEvents.shiftTime, 1);
|
appEvents.emit(CoreEvents.shiftTime, 1);
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import {
|
import { GrafanaTheme, FieldConfigSource, PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
|
||||||
GrafanaTheme,
|
|
||||||
FieldConfigSource,
|
|
||||||
PanelData,
|
|
||||||
LoadingState,
|
|
||||||
DefaultTimeRange,
|
|
||||||
PanelEvents,
|
|
||||||
SelectableValue,
|
|
||||||
TimeRange,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant, ControlledCollapse } from '@grafana/ui';
|
import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant, ControlledCollapse } 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';
|
||||||
@ -20,17 +11,278 @@ 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 } from 'react-redux';
|
import { connect, MapStateToProps, MapDispatchToProps } 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';
|
||||||
import { DisplayMode, displayModes } from './types';
|
import { DisplayMode, displayModes } from './types';
|
||||||
import { PanelEditorTabs } from './PanelEditorTabs';
|
import { PanelEditorTabs } from './PanelEditorTabs';
|
||||||
import { DashNavTimeControls } from '../DashNav/DashNavTimeControls';
|
import { DashNavTimeControls } from '../DashNav/DashNavTimeControls';
|
||||||
import { LocationState, CoreEvents } from 'app/types';
|
import { LocationState } from 'app/types';
|
||||||
import { calculatePanelSize } from './utils';
|
import { calculatePanelSize } from './utils';
|
||||||
|
import { initPanelEditor, panelEditorCleanUp } from './state/actions';
|
||||||
|
import { setDisplayMode, toggleOptionsView, setDiscardChanges } from './state/reducers';
|
||||||
import { FieldConfigEditor } from './FieldConfigEditor';
|
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 getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
const handleColor = selectThemeVariant(
|
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 { CSSProperties } from 'react';
|
||||||
import { PanelModel } from '../../state/PanelModel';
|
import { PanelModel } from '../../state/PanelModel';
|
||||||
import { GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, GRID_CELL_HEIGHT } from 'app/core/constants';
|
|
||||||
import { DisplayMode } from './types';
|
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 {
|
export function calculatePanelSize(mode: DisplayMode, width: number, height: number, panel: PanelModel): CSSProperties {
|
||||||
if (mode === DisplayMode.Fill) {
|
if (mode === DisplayMode.Fill) {
|
||||||
|
@ -12,7 +12,7 @@ import { DashboardGrid } from '../dashgrid/DashboardGrid';
|
|||||||
import { DashNav } from '../components/DashNav';
|
import { DashNav } from '../components/DashNav';
|
||||||
import { SubMenu } from '../components/SubMenu';
|
import { SubMenu } from '../components/SubMenu';
|
||||||
import { DashboardSettings } from '../components/DashboardSettings';
|
import { DashboardSettings } from '../components/DashboardSettings';
|
||||||
import PanelEditor from '../components/PanelEditor/PanelEditor';
|
import { PanelEditor } from '../components/PanelEditor/PanelEditor';
|
||||||
import { CustomScrollbar, Alert, Portal } from '@grafana/ui';
|
import { CustomScrollbar, Alert, Portal } from '@grafana/ui';
|
||||||
|
|
||||||
// Redux
|
// Redux
|
||||||
|
@ -45,6 +45,7 @@ export class DashboardModel {
|
|||||||
links: any;
|
links: any;
|
||||||
gnetId: any;
|
gnetId: any;
|
||||||
panels: PanelModel[];
|
panels: PanelModel[];
|
||||||
|
panelInEdit?: PanelModel;
|
||||||
|
|
||||||
// ------------------
|
// ------------------
|
||||||
// not persisted
|
// not persisted
|
||||||
@ -62,6 +63,7 @@ export class DashboardModel {
|
|||||||
templating: true, // needs special handling
|
templating: true, // needs special handling
|
||||||
originalTime: true,
|
originalTime: true,
|
||||||
originalTemplating: true,
|
originalTemplating: true,
|
||||||
|
panelInEdit: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(data: any, meta?: DashboardMeta) {
|
constructor(data: any, meta?: DashboardMeta) {
|
||||||
@ -221,6 +223,11 @@ export class DashboardModel {
|
|||||||
startRefresh() {
|
startRefresh() {
|
||||||
this.events.emit(PanelEvents.refresh);
|
this.events.emit(PanelEvents.refresh);
|
||||||
|
|
||||||
|
if (this.panelInEdit) {
|
||||||
|
this.panelInEdit.refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const panel of this.panels) {
|
for (const panel of this.panels) {
|
||||||
if (!this.otherPanelInFullscreen(panel)) {
|
if (!this.otherPanelInFullscreen(panel)) {
|
||||||
panel.refresh();
|
panel.refresh();
|
||||||
@ -239,15 +246,28 @@ export class DashboardModel {
|
|||||||
panelInitialized(panel: PanelModel) {
|
panelInitialized(panel: PanelModel) {
|
||||||
panel.initialized();
|
panel.initialized();
|
||||||
|
|
||||||
// In new panel edit there is no need to trigger refresh as editor retrieves last results from the query runner
|
// refresh new panels unless we are in fullscreen / edit mode
|
||||||
// as an initial value
|
if (!this.otherPanelInFullscreen(panel)) {
|
||||||
if (!this.otherPanelInFullscreen(panel) && !panel.isNewEdit) {
|
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();
|
panel.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
otherPanelInFullscreen(panel: PanelModel) {
|
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) {
|
private ensureListExist(data: any) {
|
||||||
|
@ -38,7 +38,6 @@ const notPersistedProperties: { [str: string]: boolean } = {
|
|||||||
fullscreen: true,
|
fullscreen: true,
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
isInView: true,
|
isInView: true,
|
||||||
isNewEdit: true,
|
|
||||||
hasRefreshed: true,
|
hasRefreshed: true,
|
||||||
cachedPluginOptions: true,
|
cachedPluginOptions: true,
|
||||||
plugin: true,
|
plugin: true,
|
||||||
@ -132,7 +131,6 @@ export class PanelModel {
|
|||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
isInView: boolean;
|
isInView: boolean;
|
||||||
isNewEdit: boolean;
|
|
||||||
hasRefreshed: boolean;
|
hasRefreshed: boolean;
|
||||||
events: Emitter;
|
events: Emitter;
|
||||||
cacheTimeout?: any;
|
cacheTimeout?: any;
|
||||||
@ -357,15 +355,14 @@ export class PanelModel {
|
|||||||
|
|
||||||
getEditClone() {
|
getEditClone() {
|
||||||
const clone = new PanelModel(this.getSaveModel());
|
const clone = new PanelModel(this.getSaveModel());
|
||||||
clone.queryRunner = new PanelQueryRunner();
|
const sourceQueryRunner = this.getQueryRunner();
|
||||||
|
|
||||||
// This will send the last result to the new runner
|
// pipe last result to new clone query runner
|
||||||
this.getQueryRunner()
|
sourceQueryRunner
|
||||||
.getData()
|
.getData()
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe(val => clone.queryRunner.pipeDataToSubject(val));
|
.subscribe(val => clone.getQueryRunner().pipeDataToSubject(val));
|
||||||
|
|
||||||
clone.isNewEdit = true;
|
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
} from 'app/types';
|
} from 'app/types';
|
||||||
import { processAclItems } from 'app/core/utils/acl';
|
import { processAclItems } from 'app/core/utils/acl';
|
||||||
import { panelEditorReducer } from '../panel_editor/state/reducers';
|
import { panelEditorReducer } from '../panel_editor/state/reducers';
|
||||||
|
import { panelEditorReducerNew } from '../components/PanelEditor/state/reducers';
|
||||||
import { DashboardModel } from './DashboardModel';
|
import { DashboardModel } from './DashboardModel';
|
||||||
import { PanelModel } from './PanelModel';
|
import { PanelModel } from './PanelModel';
|
||||||
|
|
||||||
@ -106,4 +107,5 @@ export const dashboardReducer = dashbardSlice.reducer;
|
|||||||
export default {
|
export default {
|
||||||
dashboard: dashboardReducer,
|
dashboard: dashboardReducer,
|
||||||
panelEditor: panelEditorReducer,
|
panelEditor: panelEditorReducer,
|
||||||
|
panelEditorNew: panelEditorReducerNew,
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@ import { PluginsState } from './plugins';
|
|||||||
import { ApplicationState } from './application';
|
import { ApplicationState } from './application';
|
||||||
import { LdapState } from './ldap';
|
import { LdapState } from './ldap';
|
||||||
import { PanelEditorState } from '../features/dashboard/panel_editor/state/reducers';
|
import { PanelEditorState } from '../features/dashboard/panel_editor/state/reducers';
|
||||||
|
import { PanelEditorStateNew } from '../features/dashboard/components/PanelEditor/state/reducers';
|
||||||
import { ApiKeysState } from './apiKeys';
|
import { ApiKeysState } from './apiKeys';
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
@ -27,6 +28,7 @@ export interface StoreState {
|
|||||||
folder: FolderState;
|
folder: FolderState;
|
||||||
dashboard: DashboardState;
|
dashboard: DashboardState;
|
||||||
panelEditor: PanelEditorState;
|
panelEditor: PanelEditorState;
|
||||||
|
panelEditorNew: PanelEditorStateNew;
|
||||||
dataSources: DataSourcesState;
|
dataSources: DataSourcesState;
|
||||||
dataSourceSettings: DataSourceSettingsState;
|
dataSourceSettings: DataSourceSettingsState;
|
||||||
explore: ExploreState;
|
explore: ExploreState;
|
||||||
|
Loading…
Reference in New Issue
Block a user