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,7 +1,7 @@
|
|||||||
import { ComponentClass } from 'react';
|
import { ComponentClass } from 'react';
|
||||||
import { LoadingState, SeriesData } from './data';
|
import { LoadingState, SeriesData } from './data';
|
||||||
import { TimeRange } from './time';
|
import { TimeRange } from './time';
|
||||||
import { ScopedVars, DataRequestInfo, DataQueryError } from './datasource';
|
import { ScopedVars, DataRequestInfo, DataQueryError, LegacyResponseData } from './datasource';
|
||||||
|
|
||||||
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
|
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
|
||||||
|
|
||||||
@@ -10,6 +10,9 @@ export interface PanelData {
|
|||||||
series: SeriesData[];
|
series: SeriesData[];
|
||||||
request?: DataRequestInfo;
|
request?: DataRequestInfo;
|
||||||
error?: DataQueryError;
|
error?: DataQueryError;
|
||||||
|
|
||||||
|
// Data format expected by Angular panels
|
||||||
|
legacy?: LegacyResponseData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelProps<T = any> {
|
export interface PanelProps<T = any> {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { PanelResizer } from './PanelResizer';
|
|||||||
import { PanelModel, DashboardModel } from '../state';
|
import { PanelModel, DashboardModel } from '../state';
|
||||||
import { PanelPlugin } from 'app/types';
|
import { PanelPlugin } from 'app/types';
|
||||||
import { AngularPanelPlugin, ReactPanelPlugin } from '@grafana/ui/src/types/panel';
|
import { AngularPanelPlugin, ReactPanelPlugin } from '@grafana/ui/src/types/panel';
|
||||||
|
import { AutoSizer } from 'react-virtualized';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@@ -153,13 +154,24 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
const { plugin } = this.state;
|
const { plugin } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelChrome
|
<AutoSizer>
|
||||||
plugin={plugin}
|
{({ width, height }) => {
|
||||||
panel={panel}
|
if (width === 0) {
|
||||||
dashboard={dashboard}
|
return null;
|
||||||
isFullscreen={isFullscreen}
|
}
|
||||||
isEditing={isEditing}
|
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' },
|
bbb: { value: 'BBB', text: 'upperB' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dashboard: {},
|
|
||||||
plugin: {},
|
|
||||||
isFullscreen: false,
|
isFullscreen: false,
|
||||||
});
|
} as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should replace a panel variable', () => {
|
it('Should replace a panel variable', () => {
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { AutoSizer } from 'react-virtualized';
|
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||||
import { DataPanel } from './DataPanel';
|
import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';
|
||||||
import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||||
@@ -19,12 +17,13 @@ import config from 'app/core/config';
|
|||||||
// Types
|
// Types
|
||||||
import { DashboardModel, PanelModel } from '../state';
|
import { DashboardModel, PanelModel } from '../state';
|
||||||
import { PanelPlugin } from 'app/types';
|
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 { ScopedVars } from '@grafana/ui';
|
||||||
|
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
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';
|
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||||
|
|
||||||
@@ -34,53 +33,152 @@ export interface Props {
|
|||||||
plugin: PanelPlugin;
|
plugin: PanelPlugin;
|
||||||
isFullscreen: boolean;
|
isFullscreen: boolean;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
refreshCounter: number;
|
isFirstLoad: boolean;
|
||||||
renderCounter: number;
|
renderCounter: number;
|
||||||
timeInfo?: string;
|
timeInfo?: string;
|
||||||
timeRange?: TimeRange;
|
timeRange?: TimeRange;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
|
|
||||||
|
// Current state of all events
|
||||||
|
data: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelChrome extends PureComponent<Props, State> {
|
export class PanelChrome extends PureComponent<Props, State> {
|
||||||
timeSrv: TimeSrv = getTimeSrv();
|
timeSrv: TimeSrv = getTimeSrv();
|
||||||
|
queryRunner = new PanelQueryRunner();
|
||||||
|
querySubscription: Unsubscribable;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
refreshCounter: 0,
|
isFirstLoad: true,
|
||||||
renderCounter: 0,
|
renderCounter: 0,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
data: {
|
||||||
|
state: LoadingState.NotStarted,
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Listen for changes to the query results
|
||||||
|
this.querySubscription = this.queryRunner.subscribe(this.panelDataObserver);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.panel.events.on('refresh', this.onRefresh);
|
const { panel, dashboard } = this.props;
|
||||||
this.props.panel.events.on('render', this.onRender);
|
panel.events.on('refresh', this.onRefresh);
|
||||||
this.props.dashboard.panelInitialized(this.props.panel);
|
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() {
|
componentWillUnmount() {
|
||||||
this.props.panel.events.off('refresh', this.onRefresh);
|
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 = () => {
|
onRefresh = () => {
|
||||||
console.log('onRefresh');
|
console.log('onRefresh');
|
||||||
if (!this.isVisible) {
|
if (!this.isVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { panel } = this.props;
|
const { panel, width } = this.props;
|
||||||
const timeData = applyPanelTimeOverrides(panel, this.timeSrv.timeRange());
|
const timeData = applyPanelTimeOverrides(panel, this.timeSrv.timeRange());
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
refreshCounter: this.state.refreshCounter + 1,
|
|
||||||
timeRange: timeData.timeRange,
|
timeRange: timeData.timeRange,
|
||||||
timeInfo: timeData.timeInfo,
|
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 = () => {
|
onRender = () => {
|
||||||
@@ -97,35 +195,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
return templateSrv.replace(value, vars, format);
|
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) => {
|
onPanelError = (message: string) => {
|
||||||
if (this.state.errorMessage !== message) {
|
if (this.state.errorMessage !== message) {
|
||||||
this.setState({ errorMessage: message });
|
this.setState({ errorMessage: message });
|
||||||
@@ -147,112 +216,82 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
return panel.snapshotData && panel.snapshotData.length;
|
return panel.snapshotData && panel.snapshotData.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
get needsQueryExecution() {
|
get wantsQueryExecution() {
|
||||||
return this.hasPanelSnapshot || this.props.plugin.dataFormats.length > 0;
|
return this.props.plugin.dataFormats.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get getDataForPanel() {
|
renderPanel(width: number, height: number): JSX.Element {
|
||||||
return {
|
|
||||||
state: LoadingState.Done,
|
|
||||||
series: this.hasPanelSnapshot ? getProcessedSeriesData(this.props.panel.snapshotData) : [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPanelPlugin(data: PanelData, width: number, height: number): JSX.Element {
|
|
||||||
const { panel, plugin } = this.props;
|
const { panel, plugin } = this.props;
|
||||||
const { timeRange, renderCounter } = this.state;
|
const { timeRange, renderCounter, data, isFirstLoad } = this.state;
|
||||||
const PanelComponent = plugin.reactPlugin.panel;
|
const PanelComponent = plugin.reactPlugin.panel;
|
||||||
|
|
||||||
// This is only done to increase a counter that is used by backend
|
// This is only done to increase a counter that is used by backend
|
||||||
// image rendering (phantomjs/headless chrome) to know when to capture image
|
// 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);
|
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 (
|
return (
|
||||||
<div className="panel-content">
|
<>
|
||||||
<PanelComponent
|
{loading === LoadingState.Loading && this.renderLoadingState()}
|
||||||
data={data}
|
<div className="panel-content">
|
||||||
timeRange={timeRange}
|
<PanelComponent
|
||||||
options={panel.getOptions(plugin.reactPlugin.defaults)}
|
data={data}
|
||||||
width={width - 2 * config.theme.panelPadding.horizontal}
|
timeRange={timeRange}
|
||||||
height={height - PANEL_HEADER_HEIGHT - config.theme.panelPadding.vertical}
|
options={panel.getOptions(plugin.reactPlugin.defaults)}
|
||||||
renderCounter={renderCounter}
|
width={width - 2 * config.theme.panelPadding.horizontal}
|
||||||
replaceVariables={this.replaceVariables}
|
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>
|
</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() {
|
render() {
|
||||||
const { dashboard, panel, isFullscreen } = this.props;
|
const { dashboard, panel, isFullscreen, width, height } = this.props;
|
||||||
const { errorMessage, timeInfo } = this.state;
|
const { errorMessage, timeInfo } = this.state;
|
||||||
const { transparent } = panel;
|
const { transparent } = panel;
|
||||||
|
|
||||||
const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
|
const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
|
||||||
return (
|
return (
|
||||||
<AutoSizer>
|
<div className={containerClassNames}>
|
||||||
{({ width, height }) => {
|
<PanelHeader
|
||||||
if (width === 0) {
|
panel={panel}
|
||||||
return null;
|
dashboard={dashboard}
|
||||||
}
|
timeInfo={timeInfo}
|
||||||
|
title={panel.title}
|
||||||
return (
|
description={panel.description}
|
||||||
<div className={containerClassNames}>
|
scopedVars={panel.scopedVars}
|
||||||
<PanelHeader
|
links={panel.links}
|
||||||
panel={panel}
|
error={errorMessage}
|
||||||
dashboard={dashboard}
|
isFullscreen={isFullscreen}
|
||||||
timeInfo={timeInfo}
|
/>
|
||||||
title={panel.title}
|
<ErrorBoundary>
|
||||||
description={panel.description}
|
{({ error, errorInfo }) => {
|
||||||
scopedVars={panel.scopedVars}
|
if (errorInfo) {
|
||||||
links={panel.links}
|
this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
|
||||||
error={errorMessage}
|
return null;
|
||||||
isFullscreen={isFullscreen}
|
}
|
||||||
/>
|
return this.renderPanel(width, height);
|
||||||
<ErrorBoundary>
|
}}
|
||||||
{({ error, errorInfo }) => {
|
</ErrorBoundary>
|
||||||
if (errorInfo) {
|
</div>
|
||||||
this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.renderPanelBody(width, height);
|
|
||||||
}}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</AutoSizer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,6 @@
|
|||||||
// Library
|
import { getProcessedSeriesData } from './PanelQueryRunner';
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
describe('QueryRunner', () => {
|
||||||
it('converts timeseries to table skipping nulls', () => {
|
it('converts timeseries to table skipping nulls', () => {
|
||||||
const input1 = {
|
const input1 = {
|
||||||
target: 'Field Name',
|
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