mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
QueryRunner: Move queryRunner to panelModel (#16656)
* move queryRunner to panelModel * remove isEditing from PanelChrome * move listener to QueriesTab * Fixed issue with isFirstLoad set to false before loading state is Done * QueryRunner: Fixed issue with error and delayed loading state indication * Anoter fix to issues with multiple setState calls in observable callbacks
This commit is contained in:
committed by
Torkel Ödegaard
parent
f4cd9bc73c
commit
0643dff2f6
@@ -37,7 +37,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
element: HTMLElement;
|
||||
specialPanels = {};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@@ -150,7 +150,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
renderReactPanel() {
|
||||
const { dashboard, panel, isFullscreen, isEditing } = this.props;
|
||||
const { dashboard, panel, isFullscreen } = this.props;
|
||||
const { plugin } = this.state;
|
||||
|
||||
return (
|
||||
@@ -165,7 +165,6 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
isFullscreen={isFullscreen}
|
||||
isEditing={isEditing}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
|
@@ -17,7 +17,7 @@ import config from 'app/core/config';
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { TimeRange, LoadingState, PanelData, toLegacyResponseData } from '@grafana/ui';
|
||||
import { TimeRange, LoadingState, PanelData } from '@grafana/ui';
|
||||
import { ScopedVars } from '@grafana/ui';
|
||||
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
@@ -32,7 +32,6 @@ export interface Props {
|
||||
dashboard: DashboardModel;
|
||||
plugin: PanelPlugin;
|
||||
isFullscreen: boolean;
|
||||
isEditing: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
@@ -50,7 +49,6 @@ export interface State {
|
||||
|
||||
export class PanelChrome extends PureComponent<Props, State> {
|
||||
timeSrv: TimeSrv = getTimeSrv();
|
||||
queryRunner = new PanelQueryRunner();
|
||||
querySubscription: Unsubscribable;
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -64,9 +62,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
series: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Listen for changes to the query results
|
||||
this.querySubscription = this.queryRunner.subscribe(this.panelDataObserver);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -91,50 +86,41 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.panel.events.off('refresh', this.onRefresh);
|
||||
if (this.querySubscription) {
|
||||
this.querySubscription.unsubscribe();
|
||||
this.querySubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the response with information from the stream
|
||||
// The next is outside a react synthetic event so setState is not batched
|
||||
// So in this context we can only do a single call to setState
|
||||
panelDataObserver = {
|
||||
next: (data: PanelData) => {
|
||||
let { errorMessage, isFirstLoad } = this.state;
|
||||
|
||||
if (data.state === LoadingState.Error) {
|
||||
const { error } = data;
|
||||
if (error) {
|
||||
let message = error.message;
|
||||
if (!message) {
|
||||
message = 'Query error';
|
||||
if (this.state.errorMessage !== error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
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();
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
// 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));
|
||||
if (data.state === LoadingState.Done) {
|
||||
// If we are doing a snapshot save data in panel model
|
||||
if (this.props.dashboard.snapshot) {
|
||||
this.props.panel.snapshotData = data.series;
|
||||
}
|
||||
if (this.state.isFirstLoad) {
|
||||
isFirstLoad = false;
|
||||
}
|
||||
|
||||
// Angular query editors expect TimeSeries|TableData
|
||||
events.emit('data-received', legacy);
|
||||
|
||||
// Notify react query editors
|
||||
events.emit('series-data-received', data);
|
||||
}
|
||||
|
||||
this.setState({ isFirstLoad, errorMessage, data });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -153,13 +139,18 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
});
|
||||
|
||||
// Issue Query
|
||||
if (this.wantsQueryExecution && !this.hasPanelSnapshot) {
|
||||
if (this.wantsQueryExecution) {
|
||||
if (width < 0) {
|
||||
console.log('No width yet... wait till we know');
|
||||
return;
|
||||
}
|
||||
|
||||
this.queryRunner.run({
|
||||
if (!panel.queryRunner) {
|
||||
panel.queryRunner = new PanelQueryRunner();
|
||||
}
|
||||
if (!this.querySubscription) {
|
||||
this.querySubscription = panel.queryRunner.subscribe(this.panelDataObserver);
|
||||
}
|
||||
panel.queryRunner.run({
|
||||
datasource: panel.datasource,
|
||||
queries: panel.targets,
|
||||
panelId: panel.id,
|
||||
@@ -195,12 +186,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
clearErrorState() {
|
||||
if (this.state.errorMessage) {
|
||||
this.setState({ errorMessage: null });
|
||||
}
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
||||
}
|
||||
@@ -211,7 +196,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
get wantsQueryExecution() {
|
||||
return this.props.plugin.dataFormats.length > 0;
|
||||
return this.props.plugin.dataFormats.length > 0 && !this.hasPanelSnapshot;
|
||||
}
|
||||
|
||||
renderPanel(width: number, height: number): JSX.Element {
|
||||
|
@@ -12,14 +12,16 @@ import { QueryEditorRow } from './QueryEditorRow';
|
||||
|
||||
// Services
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import config from 'app/core/config';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
import { DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
|
||||
import { DataQuery, DataSourceSelectItem, PanelData, LoadingState } from '@grafana/ui/src/types';
|
||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||
import { PanelQueryRunner, PanelQueryRunnerFormat } from '../state/PanelQueryRunner';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -33,11 +35,13 @@ interface State {
|
||||
isPickerOpen: boolean;
|
||||
isAddingMixed: boolean;
|
||||
scrollTop: number;
|
||||
data: PanelData;
|
||||
}
|
||||
|
||||
export class QueriesTab extends PureComponent<Props, State> {
|
||||
datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
|
||||
backendSrv: BackendSrv = getBackendSrv();
|
||||
backendSrv = getBackendSrv();
|
||||
querySubscription: Unsubscribable;
|
||||
|
||||
state: State = {
|
||||
isLoadingHelp: false,
|
||||
@@ -46,6 +50,40 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
isPickerOpen: false,
|
||||
isAddingMixed: false,
|
||||
scrollTop: 0,
|
||||
data: {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { panel } = this.props;
|
||||
|
||||
if (!panel.queryRunner) {
|
||||
panel.queryRunner = new PanelQueryRunner();
|
||||
}
|
||||
|
||||
this.querySubscription = panel.queryRunner.subscribe(this.panelDataObserver, PanelQueryRunnerFormat.both);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.querySubscription) {
|
||||
this.querySubscription.unsubscribe();
|
||||
this.querySubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the response with information from the stream
|
||||
panelDataObserver = {
|
||||
next: (data: PanelData) => {
|
||||
const { panel } = this.props;
|
||||
if (data.state === LoadingState.Error) {
|
||||
panel.events.emit('data-error', data.error);
|
||||
} else if (data.state === LoadingState.Done) {
|
||||
panel.events.emit('data-received', data.legacy);
|
||||
}
|
||||
this.setState({ data });
|
||||
},
|
||||
};
|
||||
|
||||
findCurrentDataSource(): DataSourceSelectItem {
|
||||
@@ -179,7 +217,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { panel, dashboard } = this.props;
|
||||
const { currentDS, scrollTop } = this.state;
|
||||
const { currentDS, scrollTop, data } = this.state;
|
||||
|
||||
const queryInspector: EditorToolbarView = {
|
||||
title: 'Query Inspector',
|
||||
@@ -208,6 +246,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
key={query.refId}
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
data={data}
|
||||
query={query}
|
||||
onChange={query => this.onQueryChange(query, index)}
|
||||
onRemoveQuery={this.onRemoveQuery}
|
||||
|
@@ -11,11 +11,12 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { DataQuery, DataSourceApi, TimeRange, DataQueryError, SeriesData } from '@grafana/ui';
|
||||
import { DataQuery, DataSourceApi, TimeRange, DataQueryError, SeriesData, PanelData } from '@grafana/ui';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
data: PanelData;
|
||||
query: DataQuery;
|
||||
dashboard: DashboardModel;
|
||||
onAddQuery: (query?: DataQuery) => void;
|
||||
@@ -51,61 +52,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
|
||||
componentDidMount() {
|
||||
this.loadDatasource();
|
||||
this.props.panel.events.on('refresh', this.onPanelRefresh);
|
||||
this.props.panel.events.on('data-error', this.onPanelDataError);
|
||||
this.props.panel.events.on('data-received', this.onPanelDataReceived);
|
||||
this.props.panel.events.on('series-data-received', this.onSeriesDataReceived);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.panel.events.off('refresh', this.onPanelRefresh);
|
||||
this.props.panel.events.off('data-error', this.onPanelDataError);
|
||||
this.props.panel.events.off('data-received', this.onPanelDataReceived);
|
||||
this.props.panel.events.off('series-data-received', this.onSeriesDataReceived);
|
||||
|
||||
if (this.angularQueryEditor) {
|
||||
this.angularQueryEditor.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
onPanelDataError = (error: DataQueryError) => {
|
||||
// Some query controllers listen to data error events and need a digest
|
||||
if (this.angularQueryEditor) {
|
||||
// for some reason this needs to be done in next tick
|
||||
setTimeout(this.angularQueryEditor.digest);
|
||||
return;
|
||||
}
|
||||
|
||||
// if error relates to this query store it in state and pass it on to query editor
|
||||
if (error.refId === this.props.query.refId) {
|
||||
this.setState({ queryError: error });
|
||||
}
|
||||
};
|
||||
|
||||
// Only used by angular plugins
|
||||
onPanelDataReceived = () => {
|
||||
// Some query controllers listen to data error events and need a digest
|
||||
if (this.angularQueryEditor) {
|
||||
// for some reason this needs to be done in next tick
|
||||
setTimeout(this.angularQueryEditor.digest);
|
||||
}
|
||||
};
|
||||
|
||||
// Only used by the React Query Editors
|
||||
onSeriesDataReceived = (data: SeriesData[]) => {
|
||||
if (!this.angularQueryEditor) {
|
||||
// only pass series related to this query to query editor
|
||||
const filterByRefId = data.filter(series => series.refId === this.props.query.refId);
|
||||
this.setState({ queryResponse: filterByRefId, queryError: null });
|
||||
}
|
||||
};
|
||||
|
||||
onPanelRefresh = () => {
|
||||
if (this.angularScope) {
|
||||
this.angularScope.range = getTimeSrv().timeRange();
|
||||
}
|
||||
};
|
||||
|
||||
getAngularQueryComponentScope(): AngularQueryComponentScope {
|
||||
const { panel, query, dashboard } = this.props;
|
||||
const { datasource } = this.state;
|
||||
@@ -134,8 +88,25 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { loadedDataSourceValue } = this.state;
|
||||
const { data, query } = this.props;
|
||||
|
||||
if (data !== prevProps.data) {
|
||||
const queryError = data.error && data.error.refId === query.refId ? data.error : null;
|
||||
const queryResponse = data.series.filter(series => series.refId === query.refId);
|
||||
this.setState({ queryResponse, queryError });
|
||||
|
||||
if (this.angularScope) {
|
||||
this.angularScope.range = getTimeSrv().timeRange();
|
||||
}
|
||||
|
||||
if (this.angularQueryEditor) {
|
||||
// Some query controllers listen to data error events and need a digest
|
||||
// for some reason this needs to be done in next tick
|
||||
setTimeout(this.angularQueryEditor.digest);
|
||||
}
|
||||
}
|
||||
|
||||
// check if we need to load another datasource
|
||||
if (loadedDataSourceValue !== this.props.dataSourceValue) {
|
||||
|
@@ -10,6 +10,8 @@ import { DataQuery, Threshold, ScopedVars, DataQueryResponseData } from '@grafan
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import config from 'app/core/config';
|
||||
|
||||
import { PanelQueryRunner } from './PanelQueryRunner';
|
||||
|
||||
export interface GridPos {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -25,6 +27,7 @@ const notPersistedProperties: { [str: string]: boolean } = {
|
||||
hasRefreshed: true,
|
||||
cachedPluginOptions: true,
|
||||
plugin: true,
|
||||
queryRunner: true,
|
||||
};
|
||||
|
||||
// For angular panels we need to clean up properties when changing type
|
||||
@@ -115,6 +118,7 @@ export class PanelModel {
|
||||
cachedPluginOptions?: any;
|
||||
legend?: { show: boolean };
|
||||
plugin?: PanelPlugin;
|
||||
queryRunner?: PanelQueryRunner;
|
||||
|
||||
constructor(model: any) {
|
||||
this.events = new Emitter();
|
||||
|
@@ -39,6 +39,7 @@ export interface QueryRunnerOptions<TQuery extends DataQuery = DataQuery> {
|
||||
export enum PanelQueryRunnerFormat {
|
||||
series = 'series',
|
||||
legacy = 'legacy',
|
||||
both = 'both',
|
||||
}
|
||||
|
||||
export class PanelQueryRunner {
|
||||
@@ -63,6 +64,9 @@ export class PanelQueryRunner {
|
||||
|
||||
if (format === PanelQueryRunnerFormat.legacy) {
|
||||
this.sendLegacy = true;
|
||||
} else if (format === PanelQueryRunnerFormat.both) {
|
||||
this.sendSeries = true;
|
||||
this.sendLegacy = true;
|
||||
} else {
|
||||
this.sendSeries = true;
|
||||
}
|
||||
@@ -121,6 +125,8 @@ export class PanelQueryRunner {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
let loadingStateTimeoutId = 0;
|
||||
|
||||
try {
|
||||
const ds = options.ds ? options.ds : await getDatasourceSrv().get(datasource, request.scopedVars);
|
||||
|
||||
@@ -137,18 +143,12 @@ export class PanelQueryRunner {
|
||||
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);
|
||||
loadingStateTimeoutId = window.setTimeout(() => {
|
||||
this.publishUpdate({ state: LoadingState.Loading });
|
||||
}, delayStateNotification || 500);
|
||||
|
||||
const resp = await ds.query(request);
|
||||
|
||||
request.endTime = Date.now();
|
||||
|
||||
// Make sure the response is in a supported format
|
||||
@@ -162,15 +162,16 @@ export class PanelQueryRunner {
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// The Result
|
||||
this.data = {
|
||||
// Make sure the delayed loading state timeout is cleared
|
||||
clearTimeout(loadingStateTimeoutId);
|
||||
|
||||
// Publish the result
|
||||
return this.publishUpdate({
|
||||
state: LoadingState.Done,
|
||||
series,
|
||||
legacy,
|
||||
request,
|
||||
};
|
||||
this.subject.next(this.data);
|
||||
return this.data;
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as DataQueryError;
|
||||
if (!error.message) {
|
||||
@@ -187,15 +188,26 @@ export class PanelQueryRunner {
|
||||
error.message = message;
|
||||
}
|
||||
|
||||
this.data = {
|
||||
...this.data, // ?? Should we keep existing data, or clear it ???
|
||||
// Make sure the delayed loading state timeout is cleared
|
||||
clearTimeout(loadingStateTimeoutId);
|
||||
|
||||
return this.publishUpdate({
|
||||
state: LoadingState.Error,
|
||||
error: error,
|
||||
};
|
||||
this.subject.next(this.data);
|
||||
return this.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
publishUpdate(update: Partial<PanelData>): PanelData {
|
||||
this.data = {
|
||||
...this.data,
|
||||
...update,
|
||||
};
|
||||
|
||||
this.subject.next(this.data);
|
||||
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user