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:
@@ -19,6 +19,7 @@ import { PanelResizer } from './PanelResizer';
|
||||
import { PanelModel, DashboardModel } from '../state';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { AngularPanelPlugin, ReactPanelPlugin } from '@grafana/ui/src/types/panel';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -153,13 +154,24 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
const { plugin } = this.state;
|
||||
|
||||
return (
|
||||
<PanelChrome
|
||||
plugin={plugin}
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
isFullscreen={isFullscreen}
|
||||
isEditing={isEditing}
|
||||
/>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
if (width === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<PanelChrome
|
||||
plugin={plugin}
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
isFullscreen={isFullscreen}
|
||||
isEditing={isEditing}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
// Library
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// Services
|
||||
import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
// Utils
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
// Types
|
||||
import {
|
||||
DataQueryError,
|
||||
LoadingState,
|
||||
SeriesData,
|
||||
TimeRange,
|
||||
ScopedVars,
|
||||
toSeriesData,
|
||||
guessFieldTypes,
|
||||
DataQuery,
|
||||
PanelData,
|
||||
DataRequestInfo,
|
||||
} from '@grafana/ui';
|
||||
|
||||
interface RenderProps {
|
||||
data: PanelData;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
datasource: string | null;
|
||||
queries: DataQuery[];
|
||||
panelId: number;
|
||||
dashboardId?: number;
|
||||
isVisible?: boolean;
|
||||
timeRange?: TimeRange;
|
||||
widthPixels: number;
|
||||
refreshCounter: number;
|
||||
minInterval?: string;
|
||||
maxDataPoints?: number;
|
||||
scopedVars?: ScopedVars;
|
||||
children: (r: RenderProps) => JSX.Element;
|
||||
onDataResponse?: (data?: SeriesData[]) => void;
|
||||
onError: (message: string, error: DataQueryError) => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isFirstLoad: boolean;
|
||||
data: PanelData;
|
||||
}
|
||||
|
||||
/**
|
||||
* All panels will be passed tables that have our best guess at colum type set
|
||||
*
|
||||
* This is also used by PanelChrome for snapshot support
|
||||
*/
|
||||
export function getProcessedSeriesData(results?: any[]): SeriesData[] {
|
||||
if (!results) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const series: SeriesData[] = [];
|
||||
for (const r of results) {
|
||||
if (r) {
|
||||
series.push(guessFieldTypes(toSeriesData(r)));
|
||||
}
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
export class DataPanel extends Component<Props, State> {
|
||||
static defaultProps = {
|
||||
isVisible: true,
|
||||
dashboardId: 1,
|
||||
};
|
||||
|
||||
dataSourceSrv: DatasourceSrv = getDatasourceSrv();
|
||||
isUnmounted = false;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isFirstLoad: true,
|
||||
data: {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.issueQueries();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isUnmounted = true;
|
||||
}
|
||||
|
||||
async componentDidUpdate(prevProps: Props) {
|
||||
if (!this.hasPropsChanged(prevProps)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.issueQueries();
|
||||
}
|
||||
|
||||
hasPropsChanged(prevProps: Props) {
|
||||
return this.props.refreshCounter !== prevProps.refreshCounter;
|
||||
}
|
||||
|
||||
private issueQueries = async () => {
|
||||
const {
|
||||
isVisible,
|
||||
queries,
|
||||
datasource,
|
||||
panelId,
|
||||
dashboardId,
|
||||
timeRange,
|
||||
widthPixels,
|
||||
maxDataPoints,
|
||||
scopedVars,
|
||||
onDataResponse,
|
||||
onError,
|
||||
} = this.props;
|
||||
|
||||
if (!isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!queries.length) {
|
||||
this.setState({
|
||||
data: {
|
||||
state: LoadingState.Done,
|
||||
series: [],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
data: {
|
||||
...this.state.data,
|
||||
loading: LoadingState.Loading,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const ds = await this.dataSourceSrv.get(datasource, scopedVars);
|
||||
|
||||
const minInterval = this.props.minInterval || ds.interval;
|
||||
const intervalRes = kbn.calculateInterval(timeRange, widthPixels, minInterval);
|
||||
|
||||
// make shallow copy of scoped vars,
|
||||
// and add built in variables interval and interval_ms
|
||||
const scopedVarsWithInterval = Object.assign({}, scopedVars, {
|
||||
__interval: { text: intervalRes.interval, value: intervalRes.interval },
|
||||
__interval_ms: { text: intervalRes.intervalMs.toString(), value: intervalRes.intervalMs },
|
||||
});
|
||||
|
||||
const request: DataRequestInfo = {
|
||||
timezone: 'browser',
|
||||
panelId: panelId,
|
||||
dashboardId: dashboardId,
|
||||
range: timeRange,
|
||||
rangeRaw: timeRange.raw,
|
||||
interval: intervalRes.interval,
|
||||
intervalMs: intervalRes.intervalMs,
|
||||
targets: queries,
|
||||
maxDataPoints: maxDataPoints || widthPixels,
|
||||
scopedVars: scopedVarsWithInterval,
|
||||
cacheTimeout: null,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
|
||||
const resp = await ds.query(request);
|
||||
request.endTime = Date.now();
|
||||
|
||||
if (this.isUnmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the data is SeriesData[]
|
||||
const series = getProcessedSeriesData(resp.data);
|
||||
if (onDataResponse) {
|
||||
onDataResponse(series);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isFirstLoad: false,
|
||||
data: {
|
||||
state: LoadingState.Done,
|
||||
series,
|
||||
request,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('DataPanel error', err);
|
||||
|
||||
let message = 'Query error';
|
||||
|
||||
if (err.message) {
|
||||
message = err.message;
|
||||
} else if (err.data && err.data.message) {
|
||||
message = err.data.message;
|
||||
} else if (err.data && err.data.error) {
|
||||
message = err.data.error;
|
||||
} else if (err.status) {
|
||||
message = `Query error: ${err.status} ${err.statusText}`;
|
||||
}
|
||||
|
||||
onError(message, err);
|
||||
|
||||
this.setState({
|
||||
isFirstLoad: false,
|
||||
data: {
|
||||
...this.state.data,
|
||||
loading: LoadingState.Error,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { queries } = this.props;
|
||||
const { isFirstLoad, data } = this.state;
|
||||
const { state } = data;
|
||||
|
||||
// do not render component until we have first data
|
||||
if (isFirstLoad && (state === LoadingState.Loading || state === LoadingState.NotStarted)) {
|
||||
return this.renderLoadingState();
|
||||
}
|
||||
|
||||
if (!queries.length) {
|
||||
return (
|
||||
<div className="panel-empty">
|
||||
<p>Add a query to get some data!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{state === LoadingState.Loading && this.renderLoadingState()}
|
||||
{this.props.children({ data })}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLoadingState(): JSX.Element {
|
||||
return (
|
||||
<div className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,8 @@ describe('PanelChrome', () => {
|
||||
bbb: { value: 'BBB', text: 'upperB' },
|
||||
},
|
||||
},
|
||||
dashboard: {},
|
||||
plugin: {},
|
||||
isFullscreen: false,
|
||||
});
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('Should replace a panel variable', () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,6 @@
|
||||
// Library
|
||||
import React from 'react';
|
||||
|
||||
import { DataPanel, getProcessedSeriesData } from './DataPanel';
|
||||
|
||||
describe('DataPanel', () => {
|
||||
let dataPanel: DataPanel;
|
||||
|
||||
beforeEach(() => {
|
||||
dataPanel = new DataPanel({
|
||||
queries: [],
|
||||
panelId: 1,
|
||||
widthPixels: 100,
|
||||
refreshCounter: 1,
|
||||
datasource: 'xxx',
|
||||
children: r => {
|
||||
return <div>hello</div>;
|
||||
},
|
||||
onError: (message, error) => {},
|
||||
});
|
||||
});
|
||||
|
||||
it('starts with unloaded state', () => {
|
||||
expect(dataPanel.state.isFirstLoad).toBe(true);
|
||||
});
|
||||
import { getProcessedSeriesData } from './PanelQueryRunner';
|
||||
|
||||
describe('QueryRunner', () => {
|
||||
it('converts timeseries to table skipping nulls', () => {
|
||||
const input1 = {
|
||||
target: 'Field Name',
|
||||
208
public/app/features/dashboard/state/PanelQueryRunner.ts
Normal file
208
public/app/features/dashboard/state/PanelQueryRunner.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { Subject, Unsubscribable, PartialObserver } from 'rxjs';
|
||||
import {
|
||||
guessFieldTypes,
|
||||
toSeriesData,
|
||||
PanelData,
|
||||
LoadingState,
|
||||
DataQuery,
|
||||
TimeRange,
|
||||
ScopedVars,
|
||||
DataRequestInfo,
|
||||
SeriesData,
|
||||
DataQueryError,
|
||||
toLegacyResponseData,
|
||||
isSeriesData,
|
||||
DataSourceApi,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
export interface QueryRunnerOptions {
|
||||
ds?: DataSourceApi; // if they already have the datasource, don't look it up
|
||||
datasource: string | null;
|
||||
queries: DataQuery[];
|
||||
panelId: number;
|
||||
dashboardId?: number;
|
||||
timezone?: string;
|
||||
timeRange?: TimeRange;
|
||||
widthPixels: number;
|
||||
minInterval?: string;
|
||||
maxDataPoints?: number;
|
||||
scopedVars?: ScopedVars;
|
||||
cacheTimeout?: string;
|
||||
delayStateNotification?: number; // default 100ms.
|
||||
}
|
||||
|
||||
export enum PanelQueryRunnerFormat {
|
||||
series = 'series',
|
||||
legacy = 'legacy',
|
||||
}
|
||||
|
||||
export class PanelQueryRunner {
|
||||
private subject?: Subject<PanelData>;
|
||||
|
||||
private sendSeries = false;
|
||||
private sendLegacy = false;
|
||||
|
||||
private data = {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
} as PanelData;
|
||||
|
||||
/**
|
||||
* Listen for updates to the PanelData. If a query has already run for this panel,
|
||||
* the results will be immediatly passed to the observer
|
||||
*/
|
||||
subscribe(observer: PartialObserver<PanelData>, format = PanelQueryRunnerFormat.series): Unsubscribable {
|
||||
if (!this.subject) {
|
||||
this.subject = new Subject(); // Delay creating a subject until someone is listening
|
||||
}
|
||||
|
||||
if (format === PanelQueryRunnerFormat.legacy) {
|
||||
this.sendLegacy = true;
|
||||
} else {
|
||||
this.sendSeries = true;
|
||||
}
|
||||
|
||||
// Send the last result
|
||||
if (this.data.state !== LoadingState.NotStarted) {
|
||||
// TODO: make sure it has legacy if necessary
|
||||
observer.next(this.data);
|
||||
}
|
||||
|
||||
return this.subject.subscribe(observer);
|
||||
}
|
||||
|
||||
async run(options: QueryRunnerOptions): Promise<PanelData> {
|
||||
if (!this.subject) {
|
||||
this.subject = new Subject();
|
||||
}
|
||||
|
||||
const {
|
||||
queries,
|
||||
timezone,
|
||||
datasource,
|
||||
panelId,
|
||||
dashboardId,
|
||||
timeRange,
|
||||
cacheTimeout,
|
||||
widthPixels,
|
||||
maxDataPoints,
|
||||
scopedVars,
|
||||
delayStateNotification,
|
||||
} = options;
|
||||
|
||||
const request: DataRequestInfo = {
|
||||
timezone,
|
||||
panelId,
|
||||
dashboardId,
|
||||
range: timeRange,
|
||||
rangeRaw: timeRange.raw,
|
||||
interval: '',
|
||||
intervalMs: 0,
|
||||
targets: cloneDeep(queries),
|
||||
maxDataPoints: maxDataPoints || widthPixels,
|
||||
scopedVars: scopedVars || {},
|
||||
cacheTimeout,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
|
||||
if (!queries) {
|
||||
this.data = {
|
||||
state: LoadingState.Done,
|
||||
series: [], // Clear the data
|
||||
legacy: [],
|
||||
request,
|
||||
};
|
||||
this.subject.next(this.data);
|
||||
return this.data;
|
||||
}
|
||||
|
||||
try {
|
||||
const ds = options.ds ? options.ds : await getDatasourceSrv().get(datasource, request.scopedVars);
|
||||
|
||||
const minInterval = options.minInterval || ds.interval;
|
||||
const norm = kbn.calculateInterval(timeRange, widthPixels, minInterval);
|
||||
|
||||
// make shallow copy of scoped vars,
|
||||
// and add built in variables interval and interval_ms
|
||||
request.scopedVars = Object.assign({}, request.scopedVars, {
|
||||
__interval: { text: norm.interval, value: norm.interval },
|
||||
__interval_ms: { text: norm.intervalMs, value: norm.intervalMs },
|
||||
});
|
||||
request.interval = norm.interval;
|
||||
request.intervalMs = norm.intervalMs;
|
||||
|
||||
// Send a loading status event on slower queries
|
||||
setTimeout(() => {
|
||||
if (!request.endTime) {
|
||||
this.data = {
|
||||
...this.data,
|
||||
state: LoadingState.Loading,
|
||||
request,
|
||||
};
|
||||
this.subject.next(this.data);
|
||||
}
|
||||
}, delayStateNotification || 100);
|
||||
|
||||
const resp = await ds.query(request);
|
||||
request.endTime = Date.now();
|
||||
|
||||
// Make sure the response is in a supported format
|
||||
const series = this.sendSeries ? getProcessedSeriesData(resp.data) : [];
|
||||
const legacy = this.sendLegacy
|
||||
? resp.data.map(v => {
|
||||
if (isSeriesData(v)) {
|
||||
return toLegacyResponseData(v);
|
||||
}
|
||||
return v;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// The Result
|
||||
this.data = {
|
||||
state: LoadingState.Done,
|
||||
series,
|
||||
legacy,
|
||||
request,
|
||||
};
|
||||
this.subject.next(this.data);
|
||||
return this.data;
|
||||
} catch (err) {
|
||||
const error = err as DataQueryError;
|
||||
if (!error.message) {
|
||||
err.message = 'Query Error';
|
||||
}
|
||||
|
||||
this.data = {
|
||||
...this.data, // ?? Should we keep existing data, or clear it ???
|
||||
state: LoadingState.Error,
|
||||
error: error,
|
||||
};
|
||||
this.subject.next(this.data);
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All panels will be passed tables that have our best guess at colum type set
|
||||
*
|
||||
* This is also used by PanelChrome for snapshot support
|
||||
*/
|
||||
export function getProcessedSeriesData(results?: any[]): SeriesData[] {
|
||||
if (!results) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const series: SeriesData[] = [];
|
||||
for (const r of results) {
|
||||
if (r) {
|
||||
series.push(guessFieldTypes(toSeriesData(r)));
|
||||
}
|
||||
}
|
||||
return series;
|
||||
}
|
||||
Reference in New Issue
Block a user