mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
refactor: Merge PanelChrome and DataPanel, do query execution in PanelQueryRunner (#16632)
Moves query execution logic to PanelQueryRunner and structures PanelChrome so it subscribes to the query results rather than necessarily controlling their execution.
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
|
||||
// Services
|
||||
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
||||
|
||||
// Components
|
||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||
import { DataPanel } from './DataPanel';
|
||||
import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
|
||||
import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';
|
||||
|
||||
// Utils
|
||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||
@@ -19,12 +17,13 @@ import config from 'app/core/config';
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { TimeRange, LoadingState, DataQueryError, SeriesData, toLegacyResponseData, PanelData } from '@grafana/ui';
|
||||
import { TimeRange, LoadingState, PanelData, toLegacyResponseData } from '@grafana/ui';
|
||||
import { ScopedVars } from '@grafana/ui';
|
||||
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
|
||||
import { getProcessedSeriesData } from './DataPanel';
|
||||
import { PanelQueryRunner, getProcessedSeriesData } from '../state/PanelQueryRunner';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||
|
||||
@@ -34,53 +33,152 @@ export interface Props {
|
||||
plugin: PanelPlugin;
|
||||
isFullscreen: boolean;
|
||||
isEditing: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
refreshCounter: number;
|
||||
isFirstLoad: boolean;
|
||||
renderCounter: number;
|
||||
timeInfo?: string;
|
||||
timeRange?: TimeRange;
|
||||
errorMessage: string | null;
|
||||
|
||||
// Current state of all events
|
||||
data: PanelData;
|
||||
}
|
||||
|
||||
export class PanelChrome extends PureComponent<Props, State> {
|
||||
timeSrv: TimeSrv = getTimeSrv();
|
||||
queryRunner = new PanelQueryRunner();
|
||||
querySubscription: Unsubscribable;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
refreshCounter: 0,
|
||||
isFirstLoad: true,
|
||||
renderCounter: 0,
|
||||
errorMessage: null,
|
||||
data: {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Listen for changes to the query results
|
||||
this.querySubscription = this.queryRunner.subscribe(this.panelDataObserver);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.panel.events.on('refresh', this.onRefresh);
|
||||
this.props.panel.events.on('render', this.onRender);
|
||||
this.props.dashboard.panelInitialized(this.props.panel);
|
||||
const { panel, dashboard } = this.props;
|
||||
panel.events.on('refresh', this.onRefresh);
|
||||
panel.events.on('render', this.onRender);
|
||||
dashboard.panelInitialized(this.props.panel);
|
||||
|
||||
// Move snapshot data into the query response
|
||||
if (this.hasPanelSnapshot) {
|
||||
this.setState({
|
||||
data: {
|
||||
state: LoadingState.Done,
|
||||
series: getProcessedSeriesData(panel.snapshotData),
|
||||
},
|
||||
isFirstLoad: false,
|
||||
});
|
||||
} else if (!this.wantsQueryExecution) {
|
||||
this.setState({ isFirstLoad: false });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.panel.events.off('refresh', this.onRefresh);
|
||||
}
|
||||
|
||||
// Updates the response with information from the stream
|
||||
panelDataObserver = {
|
||||
next: (data: PanelData) => {
|
||||
if (data.state === LoadingState.Error) {
|
||||
const { error } = data;
|
||||
if (error) {
|
||||
let message = 'Query error';
|
||||
if (error.message) {
|
||||
message = error.message;
|
||||
} else if (error.data && error.data.message) {
|
||||
message = error.data.message;
|
||||
} else if (error.data && error.data.error) {
|
||||
message = error.data.error;
|
||||
} else if (error.status) {
|
||||
message = `Query error: ${error.status} ${error.statusText}`;
|
||||
}
|
||||
|
||||
if (this.state.errorMessage !== message) {
|
||||
this.setState({ errorMessage: message });
|
||||
}
|
||||
// this event is used by old query editors
|
||||
this.props.panel.events.emit('data-error', error);
|
||||
}
|
||||
} else {
|
||||
this.clearErrorState();
|
||||
}
|
||||
|
||||
// Save the query response into the panel
|
||||
if (data.state === LoadingState.Done && this.props.dashboard.snapshot) {
|
||||
this.props.panel.snapshotData = data.series;
|
||||
}
|
||||
|
||||
this.setState({ data, isFirstLoad: false });
|
||||
|
||||
// Notify query editors that the results have changed
|
||||
if (this.props.isEditing) {
|
||||
const events = this.props.panel.events;
|
||||
let legacy = data.legacy;
|
||||
if (!legacy) {
|
||||
legacy = data.series.map(v => toLegacyResponseData(v));
|
||||
}
|
||||
|
||||
// Angular query editors expect TimeSeries|TableData
|
||||
events.emit('data-received', legacy);
|
||||
|
||||
// Notify react query editors
|
||||
events.emit('series-data-received', data);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onRefresh = () => {
|
||||
console.log('onRefresh');
|
||||
if (!this.isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { panel } = this.props;
|
||||
const { panel, width } = this.props;
|
||||
const timeData = applyPanelTimeOverrides(panel, this.timeSrv.timeRange());
|
||||
|
||||
this.setState({
|
||||
refreshCounter: this.state.refreshCounter + 1,
|
||||
timeRange: timeData.timeRange,
|
||||
timeInfo: timeData.timeInfo,
|
||||
});
|
||||
|
||||
// Issue Query
|
||||
if (this.wantsQueryExecution && !this.hasPanelSnapshot) {
|
||||
if (width < 0) {
|
||||
console.log('No width yet... wait till we know');
|
||||
return;
|
||||
}
|
||||
|
||||
this.queryRunner.run({
|
||||
datasource: panel.datasource,
|
||||
queries: panel.targets,
|
||||
panelId: panel.id,
|
||||
dashboardId: this.props.dashboard.id,
|
||||
timezone: this.props.dashboard.timezone,
|
||||
timeRange: timeData.timeRange,
|
||||
widthPixels: width,
|
||||
minInterval: undefined, // Currently not passed in DataPanel?
|
||||
maxDataPoints: panel.maxDataPoints,
|
||||
scopedVars: panel.scopedVars,
|
||||
cacheTimeout: panel.cacheTimeout,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRender = () => {
|
||||
@@ -97,35 +195,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
return templateSrv.replace(value, vars, format);
|
||||
};
|
||||
|
||||
onDataResponse = (data?: SeriesData[]) => {
|
||||
if (this.props.dashboard.isSnapshot()) {
|
||||
this.props.panel.snapshotData = data;
|
||||
}
|
||||
// clear error state (if any)
|
||||
this.clearErrorState();
|
||||
|
||||
if (this.props.isEditing) {
|
||||
const events = this.props.panel.events;
|
||||
if (!data) {
|
||||
data = [];
|
||||
}
|
||||
|
||||
// Angular query editors expect TimeSeries|TableData
|
||||
events.emit('data-received', data.map(v => toLegacyResponseData(v)));
|
||||
|
||||
// Notify react query editors
|
||||
events.emit('series-data-received', data);
|
||||
}
|
||||
};
|
||||
|
||||
onDataError = (message: string, error: DataQueryError) => {
|
||||
if (this.state.errorMessage !== message) {
|
||||
this.setState({ errorMessage: message });
|
||||
}
|
||||
// this event is used by old query editors
|
||||
this.props.panel.events.emit('data-error', error);
|
||||
};
|
||||
|
||||
onPanelError = (message: string) => {
|
||||
if (this.state.errorMessage !== message) {
|
||||
this.setState({ errorMessage: message });
|
||||
@@ -147,112 +216,82 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
return panel.snapshotData && panel.snapshotData.length;
|
||||
}
|
||||
|
||||
get needsQueryExecution() {
|
||||
return this.hasPanelSnapshot || this.props.plugin.dataFormats.length > 0;
|
||||
get wantsQueryExecution() {
|
||||
return this.props.plugin.dataFormats.length > 0;
|
||||
}
|
||||
|
||||
get getDataForPanel() {
|
||||
return {
|
||||
state: LoadingState.Done,
|
||||
series: this.hasPanelSnapshot ? getProcessedSeriesData(this.props.panel.snapshotData) : [],
|
||||
};
|
||||
}
|
||||
|
||||
renderPanelPlugin(data: PanelData, width: number, height: number): JSX.Element {
|
||||
renderPanel(width: number, height: number): JSX.Element {
|
||||
const { panel, plugin } = this.props;
|
||||
const { timeRange, renderCounter } = this.state;
|
||||
const { timeRange, renderCounter, data, isFirstLoad } = this.state;
|
||||
const PanelComponent = plugin.reactPlugin.panel;
|
||||
|
||||
// This is only done to increase a counter that is used by backend
|
||||
// image rendering (phantomjs/headless chrome) to know when to capture image
|
||||
if (data.state === LoadingState.Done) {
|
||||
const loading = data.state;
|
||||
if (loading === LoadingState.Done) {
|
||||
profiler.renderingCompleted(panel.id);
|
||||
}
|
||||
|
||||
// do not render component until we have first data
|
||||
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
|
||||
return this.renderLoadingState();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel-content">
|
||||
<PanelComponent
|
||||
data={data}
|
||||
timeRange={timeRange}
|
||||
options={panel.getOptions(plugin.reactPlugin.defaults)}
|
||||
width={width - 2 * config.theme.panelPadding.horizontal}
|
||||
height={height - PANEL_HEADER_HEIGHT - config.theme.panelPadding.vertical}
|
||||
renderCounter={renderCounter}
|
||||
replaceVariables={this.replaceVariables}
|
||||
/>
|
||||
<>
|
||||
{loading === LoadingState.Loading && this.renderLoadingState()}
|
||||
<div className="panel-content">
|
||||
<PanelComponent
|
||||
data={data}
|
||||
timeRange={timeRange}
|
||||
options={panel.getOptions(plugin.reactPlugin.defaults)}
|
||||
width={width - 2 * config.theme.panelPadding.horizontal}
|
||||
height={height - PANEL_HEADER_HEIGHT - config.theme.panelPadding.vertical}
|
||||
renderCounter={renderCounter}
|
||||
replaceVariables={this.replaceVariables}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLoadingState(): JSX.Element {
|
||||
return (
|
||||
<div className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPanelBody = (width: number, height: number): JSX.Element => {
|
||||
const { panel } = this.props;
|
||||
const { refreshCounter, timeRange } = this.state;
|
||||
const { datasource, targets } = panel;
|
||||
return (
|
||||
<>
|
||||
{this.needsQueryExecution ? (
|
||||
<DataPanel
|
||||
panelId={panel.id}
|
||||
datasource={datasource}
|
||||
queries={targets}
|
||||
timeRange={timeRange}
|
||||
isVisible={this.isVisible}
|
||||
widthPixels={width}
|
||||
refreshCounter={refreshCounter}
|
||||
scopedVars={panel.scopedVars}
|
||||
onDataResponse={this.onDataResponse}
|
||||
onError={this.onDataError}
|
||||
>
|
||||
{({ data }) => {
|
||||
return this.renderPanelPlugin(data, width, height);
|
||||
}}
|
||||
</DataPanel>
|
||||
) : (
|
||||
this.renderPanelPlugin(this.getDataForPanel, width, height)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, panel, isFullscreen } = this.props;
|
||||
const { dashboard, panel, isFullscreen, width, height } = this.props;
|
||||
const { errorMessage, timeInfo } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
if (width === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={containerClassNames}>
|
||||
<PanelHeader
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
timeInfo={timeInfo}
|
||||
title={panel.title}
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
error={errorMessage}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
{({ error, errorInfo }) => {
|
||||
if (errorInfo) {
|
||||
this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
|
||||
return null;
|
||||
}
|
||||
return this.renderPanelBody(width, height);
|
||||
}}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
<div className={containerClassNames}>
|
||||
<PanelHeader
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
timeInfo={timeInfo}
|
||||
title={panel.title}
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
error={errorMessage}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
{({ error, errorInfo }) => {
|
||||
if (errorInfo) {
|
||||
this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
|
||||
return null;
|
||||
}
|
||||
return this.renderPanel(width, height);
|
||||
}}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user