2020-02-11 06:48:36 -06:00
|
|
|
import React, { PureComponent } from 'react';
|
2020-02-09 03:50:58 -06:00
|
|
|
import {
|
2020-02-09 08:39:46 -06:00
|
|
|
GrafanaTheme,
|
|
|
|
FieldConfigSource,
|
|
|
|
PanelData,
|
|
|
|
LoadingState,
|
|
|
|
DefaultTimeRange,
|
|
|
|
PanelEvents,
|
|
|
|
SelectableValue,
|
2020-02-09 11:48:14 -06:00
|
|
|
TimeRange,
|
2020-02-09 08:39:46 -06:00
|
|
|
} from '@grafana/data';
|
2020-02-11 06:48:36 -06:00
|
|
|
import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant, ControlledCollapse } from '@grafana/ui';
|
2020-02-08 05:01:10 -06:00
|
|
|
import { css, cx } from 'emotion';
|
2019-12-16 02:18:48 -06:00
|
|
|
import config from 'app/core/config';
|
2020-02-09 08:39:46 -06:00
|
|
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
2019-12-16 02:18:48 -06:00
|
|
|
|
|
|
|
import { PanelModel } from '../../state/PanelModel';
|
|
|
|
import { DashboardModel } from '../../state/DashboardModel';
|
|
|
|
import { DashboardPanel } from '../../dashgrid/DashboardPanel';
|
2020-02-09 08:39:46 -06:00
|
|
|
|
2020-02-08 05:01:10 -06:00
|
|
|
import SplitPane from 'react-split-pane';
|
2020-02-07 07:59:04 -06:00
|
|
|
import { StoreState } from '../../../../types/store';
|
|
|
|
import { connect } from 'react-redux';
|
|
|
|
import { updateLocation } from '../../../../core/reducers/location';
|
2020-02-08 06:23:16 -06:00
|
|
|
import { Unsubscribable } from 'rxjs';
|
2020-02-08 12:31:17 -06:00
|
|
|
import { PanelTitle } from './PanelTitle';
|
2020-02-09 08:39:46 -06:00
|
|
|
import { DisplayMode, displayModes } from './types';
|
|
|
|
import { PanelEditorTabs } from './PanelEditorTabs';
|
2020-02-09 11:48:14 -06:00
|
|
|
import { DashNavTimeControls } from '../DashNav/DashNavTimeControls';
|
|
|
|
import { LocationState, CoreEvents } from 'app/types';
|
2020-02-11 06:48:36 -06:00
|
|
|
import { calculatePanelSize } from './utils';
|
|
|
|
import { FieldConfigEditor } from './FieldConfigEditor';
|
2019-12-16 02:18:48 -06:00
|
|
|
|
2020-02-08 05:01:10 -06:00
|
|
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
2020-02-08 12:31:17 -06:00
|
|
|
const handleColor = selectThemeVariant(
|
|
|
|
{
|
|
|
|
dark: theme.colors.dark9,
|
|
|
|
light: theme.colors.gray6,
|
|
|
|
},
|
|
|
|
theme.type
|
|
|
|
);
|
|
|
|
|
2020-02-08 05:01:10 -06:00
|
|
|
const resizer = css`
|
|
|
|
padding: 3px;
|
|
|
|
font-style: italic;
|
|
|
|
background: ${theme.colors.panelBg};
|
|
|
|
&:hover {
|
2020-02-08 12:31:17 -06:00
|
|
|
background: ${handleColor};
|
2020-02-08 05:01:10 -06:00
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
return {
|
|
|
|
wrapper: css`
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
position: fixed;
|
|
|
|
z-index: ${theme.zIndex.modal};
|
|
|
|
top: 0;
|
|
|
|
left: 0;
|
|
|
|
right: 0;
|
|
|
|
bottom: 0;
|
|
|
|
background: ${theme.colors.pageBg};
|
|
|
|
`,
|
2020-02-09 08:39:46 -06:00
|
|
|
panelWrapper: css`
|
2020-02-08 05:01:10 -06:00
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
`,
|
|
|
|
resizerV: cx(
|
|
|
|
resizer,
|
|
|
|
css`
|
|
|
|
cursor: col-resize;
|
|
|
|
`
|
|
|
|
),
|
|
|
|
resizerH: cx(
|
|
|
|
resizer,
|
|
|
|
css`
|
|
|
|
cursor: row-resize;
|
|
|
|
`
|
|
|
|
),
|
2020-02-08 07:12:02 -06:00
|
|
|
noScrollPaneContent: css`
|
|
|
|
height: 100%;
|
2020-02-08 12:31:17 -06:00
|
|
|
width: 100%;
|
2020-02-08 07:12:02 -06:00
|
|
|
overflow: hidden;
|
|
|
|
`,
|
2020-02-08 12:31:17 -06:00
|
|
|
toolbar: css`
|
|
|
|
padding: ${theme.spacing.sm};
|
|
|
|
height: 48px;
|
|
|
|
display: flex;
|
|
|
|
justify-content: space-between;
|
|
|
|
`,
|
|
|
|
panes: css`
|
|
|
|
height: calc(100% - 48px);
|
|
|
|
position: relative;
|
|
|
|
`,
|
|
|
|
toolbarLeft: css`
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
`,
|
2020-02-09 08:39:46 -06:00
|
|
|
centeringContainer: css`
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
align-items: center;
|
|
|
|
`,
|
2020-02-08 05:01:10 -06:00
|
|
|
};
|
|
|
|
});
|
2019-12-16 02:18:48 -06:00
|
|
|
|
|
|
|
interface Props {
|
|
|
|
dashboard: DashboardModel;
|
2020-02-08 11:29:09 -06:00
|
|
|
sourcePanel: PanelModel;
|
2020-02-07 07:59:04 -06:00
|
|
|
updateLocation: typeof updateLocation;
|
2020-02-09 11:48:14 -06:00
|
|
|
location: LocationState;
|
2019-12-16 02:18:48 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
interface State {
|
2020-02-08 06:23:16 -06:00
|
|
|
pluginLoadedCounter: number;
|
2020-02-08 11:29:09 -06:00
|
|
|
panel: PanelModel;
|
2020-02-08 06:23:16 -06:00
|
|
|
data: PanelData;
|
2020-02-09 08:39:46 -06:00
|
|
|
mode: DisplayMode;
|
|
|
|
showPanelOptions: boolean;
|
2019-12-16 02:18:48 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
export class PanelEditor extends PureComponent<Props, State> {
|
2020-02-08 06:23:16 -06:00
|
|
|
querySubscription: Unsubscribable;
|
|
|
|
|
2020-02-07 07:59:04 -06:00
|
|
|
constructor(props: Props) {
|
|
|
|
super(props);
|
2020-02-08 06:23:16 -06:00
|
|
|
|
|
|
|
// To ensure visualisation settings are re-rendered when plugin has loaded
|
|
|
|
// panelInitialised event is emmited from PanelChrome
|
2020-02-08 11:29:09 -06:00
|
|
|
const panel = props.sourcePanel.getEditClone();
|
|
|
|
this.state = {
|
|
|
|
panel,
|
|
|
|
pluginLoadedCounter: 0,
|
2020-02-09 08:39:46 -06:00
|
|
|
mode: DisplayMode.Fill,
|
|
|
|
showPanelOptions: true,
|
2020-02-08 11:29:09 -06:00
|
|
|
data: {
|
|
|
|
state: LoadingState.NotStarted,
|
|
|
|
series: [],
|
|
|
|
timeRange: DefaultTimeRange,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
const { sourcePanel } = this.props;
|
|
|
|
const { panel } = this.state;
|
|
|
|
panel.events.on(PanelEvents.panelInitialized, () => {
|
2020-02-09 11:48:14 -06:00
|
|
|
const { panel } = this.state;
|
|
|
|
if (panel.angularPanel) {
|
|
|
|
console.log('Refresh angular panel in new editor');
|
|
|
|
}
|
2020-02-08 06:23:16 -06:00
|
|
|
this.setState(state => ({
|
|
|
|
pluginLoadedCounter: state.pluginLoadedCounter + 1,
|
|
|
|
}));
|
|
|
|
});
|
2020-02-09 11:48:14 -06:00
|
|
|
|
|
|
|
// Get data from any pending queries
|
2020-02-08 11:29:09 -06:00
|
|
|
sourcePanel
|
2020-02-08 06:23:16 -06:00
|
|
|
.getQueryRunner()
|
|
|
|
.getData()
|
|
|
|
.subscribe({
|
|
|
|
next: (data: PanelData) => {
|
|
|
|
this.setState({ data });
|
|
|
|
// TODO, cancel????
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
// Listen for queries on the new panel
|
2020-02-08 11:29:09 -06:00
|
|
|
const queryRunner = panel.getQueryRunner();
|
2020-02-08 06:23:16 -06:00
|
|
|
this.querySubscription = queryRunner.getData().subscribe({
|
|
|
|
next: (data: PanelData) => this.setState({ data }),
|
|
|
|
});
|
2020-02-09 11:48:14 -06:00
|
|
|
|
|
|
|
// Listen to timepicker changes
|
|
|
|
this.props.dashboard.on(CoreEvents.timeRangeUpdated, this.onTimeRangeUpdated);
|
2020-02-08 06:23:16 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
if (this.querySubscription) {
|
|
|
|
this.querySubscription.unsubscribe();
|
|
|
|
}
|
|
|
|
//this.cleanUpAngularOptions();
|
2020-02-09 11:48:14 -06:00
|
|
|
|
|
|
|
// Remove the time listener
|
|
|
|
this.props.dashboard.off(CoreEvents.timeRangeUpdated, this.onTimeRangeUpdated);
|
2020-02-07 07:59:04 -06:00
|
|
|
}
|
2019-12-16 02:18:48 -06:00
|
|
|
|
2020-02-09 11:48:14 -06:00
|
|
|
onTimeRangeUpdated = (timeRange: TimeRange) => {
|
|
|
|
const { panel } = this.state;
|
|
|
|
if (panel) {
|
|
|
|
panel.refresh();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-02-07 07:59:04 -06:00
|
|
|
onPanelUpdate = () => {
|
2020-02-08 11:29:09 -06:00
|
|
|
const { panel } = this.state;
|
2020-02-07 07:59:04 -06:00
|
|
|
const { dashboard } = this.props;
|
2020-02-08 11:29:09 -06:00
|
|
|
dashboard.updatePanel(panel);
|
2020-02-07 07:59:04 -06:00
|
|
|
};
|
2019-12-16 02:18:48 -06:00
|
|
|
|
2020-02-07 07:59:04 -06:00
|
|
|
onPanelExit = () => {
|
|
|
|
const { updateLocation } = this.props;
|
|
|
|
this.onPanelUpdate();
|
|
|
|
updateLocation({
|
|
|
|
query: { editPanel: null },
|
|
|
|
partial: true,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
onDiscard = () => {
|
|
|
|
this.props.updateLocation({
|
|
|
|
query: { editPanel: null },
|
|
|
|
partial: true,
|
|
|
|
});
|
|
|
|
};
|
2019-12-16 02:18:48 -06:00
|
|
|
|
2020-02-08 11:29:09 -06:00
|
|
|
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 (
|
2020-02-09 10:39:26 -06:00
|
|
|
<FieldConfigEditor
|
|
|
|
config={fieldOptions}
|
|
|
|
custom={plugin.customFieldConfigs}
|
|
|
|
onChange={this.onFieldConfigsChange}
|
|
|
|
data={data.series}
|
|
|
|
/>
|
2020-02-08 11:29:09 -06:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2020-02-09 07:34:42 -06:00
|
|
|
return (
|
2020-02-09 10:39:26 -06:00
|
|
|
<div style={{ marginTop: '10px' }}>
|
2020-02-09 07:34:42 -06:00
|
|
|
<plugin.editor data={data} options={panel.getOptions()} onOptionsChange={this.onPanelOptionsChanged} />
|
|
|
|
</div>
|
|
|
|
);
|
2020-02-08 11:29:09 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return <div>No editor (angular?)</div>;
|
|
|
|
}
|
|
|
|
|
2020-02-08 05:01:10 -06:00
|
|
|
onDragFinished = () => {
|
|
|
|
document.body.style.cursor = 'auto';
|
|
|
|
console.log('TODO, save splitter settings');
|
|
|
|
};
|
|
|
|
|
2020-02-08 12:31:17 -06:00
|
|
|
onPanelTitleChange = (title: string) => {
|
|
|
|
this.state.panel.title = title;
|
|
|
|
this.forceUpdate();
|
|
|
|
};
|
|
|
|
|
2020-02-09 08:39:46 -06:00
|
|
|
onDiplayModeChange = (mode: SelectableValue<DisplayMode>) => {
|
|
|
|
this.setState({
|
|
|
|
mode: mode.value!,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
onTogglePanelOptions = () => {
|
|
|
|
this.setState({
|
|
|
|
showPanelOptions: !this.state.showPanelOptions,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
renderHorizontalSplit(styles: any) {
|
2020-02-09 03:50:58 -06:00
|
|
|
const { dashboard } = this.props;
|
2020-02-09 08:39:46 -06:00
|
|
|
const { panel, mode } = this.state;
|
2020-02-09 03:50:58 -06:00
|
|
|
|
|
|
|
return (
|
2020-02-09 08:39:46 -06:00
|
|
|
<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;
|
|
|
|
}
|
2020-02-09 03:50:58 -06:00
|
|
|
return (
|
2020-02-09 08:39:46 -06:00
|
|
|
<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>
|
2020-02-09 03:50:58 -06:00
|
|
|
);
|
2020-02-09 08:39:46 -06:00
|
|
|
}}
|
|
|
|
</AutoSizer>
|
|
|
|
</div>
|
|
|
|
<div className={styles.noScrollPaneContent}>
|
|
|
|
<PanelEditorTabs panel={panel} dashboard={dashboard} />
|
|
|
|
</div>
|
|
|
|
</SplitPane>
|
2020-02-09 03:50:58 -06:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-12-16 02:18:48 -06:00
|
|
|
render() {
|
2020-02-09 11:48:14 -06:00
|
|
|
const { dashboard, location } = this.props;
|
2020-02-09 08:39:46 -06:00
|
|
|
const { panel, mode, showPanelOptions } = this.state;
|
2019-12-16 02:18:48 -06:00
|
|
|
const styles = getStyles(config.theme);
|
|
|
|
|
2020-02-08 11:29:09 -06:00
|
|
|
if (!panel) {
|
2019-12-16 02:18:48 -06:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2020-02-08 05:01:10 -06:00
|
|
|
<div className={styles.wrapper}>
|
2020-02-08 12:31:17 -06:00
|
|
|
<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>
|
2020-02-09 08:39:46 -06:00
|
|
|
<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} />
|
2020-02-08 12:31:17 -06:00
|
|
|
<Forms.Button variant="destructive" onClick={this.onDiscard}>
|
|
|
|
Discard
|
|
|
|
</Forms.Button>
|
2020-02-09 11:48:14 -06:00
|
|
|
|
|
|
|
<div>
|
|
|
|
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
|
|
|
|
</div>
|
2020-02-08 12:31:17 -06:00
|
|
|
</div>
|
2020-02-08 05:01:10 -06:00
|
|
|
</div>
|
2020-02-08 12:31:17 -06:00
|
|
|
<div className={styles.panes}>
|
2020-02-09 08:39:46 -06:00
|
|
|
{showPanelOptions ? (
|
2020-02-08 12:31:17 -06:00
|
|
|
<SplitPane
|
2020-02-09 08:39:46 -06:00
|
|
|
split="vertical"
|
2020-02-09 03:50:58 -06:00
|
|
|
minSize={100}
|
2020-02-08 12:31:17 -06:00
|
|
|
primary="second"
|
2020-02-09 08:39:46 -06:00
|
|
|
defaultSize={350}
|
|
|
|
resizerClassName={styles.resizerV}
|
|
|
|
onDragStarted={() => (document.body.style.cursor = 'col-resize')}
|
2020-02-08 12:31:17 -06:00
|
|
|
onDragFinished={this.onDragFinished}
|
|
|
|
>
|
2020-02-09 08:39:46 -06:00
|
|
|
{this.renderHorizontalSplit(styles)}
|
|
|
|
<div className={styles.noScrollPaneContent}>
|
|
|
|
<CustomScrollbar>
|
|
|
|
<div style={{ padding: '10px' }}>
|
|
|
|
{this.renderFieldOptions()}
|
2020-02-09 10:39:26 -06:00
|
|
|
<ControlledCollapse label="Visualization Settings" collapsible>
|
|
|
|
{this.renderVisSettings()}
|
|
|
|
</ControlledCollapse>
|
2020-02-09 08:39:46 -06:00
|
|
|
</div>
|
|
|
|
</CustomScrollbar>
|
2020-02-08 12:31:17 -06:00
|
|
|
</div>
|
|
|
|
</SplitPane>
|
2020-02-09 08:39:46 -06:00
|
|
|
) : (
|
|
|
|
this.renderHorizontalSplit(styles)
|
|
|
|
)}
|
2020-02-08 12:31:17 -06:00
|
|
|
</div>
|
2020-02-08 05:01:10 -06:00
|
|
|
</div>
|
2019-12-16 02:18:48 -06:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2020-02-07 07:59:04 -06:00
|
|
|
|
|
|
|
const mapStateToProps = (state: StoreState) => ({
|
|
|
|
location: state.location,
|
|
|
|
});
|
|
|
|
|
|
|
|
const mapDispatchToProps = {
|
|
|
|
updateLocation,
|
|
|
|
};
|
|
|
|
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(PanelEditor);
|