Commit 0643dff2 by Ryan McKinley Committed by Torkel Ödegaard

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
parent f4cd9bc7
...@@ -37,7 +37,7 @@ export class DashboardPanel extends PureComponent<Props, State> { ...@@ -37,7 +37,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
element: HTMLElement; element: HTMLElement;
specialPanels = {}; specialPanels = {};
constructor(props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
...@@ -150,7 +150,7 @@ export class DashboardPanel extends PureComponent<Props, State> { ...@@ -150,7 +150,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
}; };
renderReactPanel() { renderReactPanel() {
const { dashboard, panel, isFullscreen, isEditing } = this.props; const { dashboard, panel, isFullscreen } = this.props;
const { plugin } = this.state; const { plugin } = this.state;
return ( return (
...@@ -165,7 +165,6 @@ export class DashboardPanel extends PureComponent<Props, State> { ...@@ -165,7 +165,6 @@ export class DashboardPanel extends PureComponent<Props, State> {
panel={panel} panel={panel}
dashboard={dashboard} dashboard={dashboard}
isFullscreen={isFullscreen} isFullscreen={isFullscreen}
isEditing={isEditing}
width={width} width={width}
height={height} height={height}
/> />
......
...@@ -17,7 +17,7 @@ import config from 'app/core/config'; ...@@ -17,7 +17,7 @@ 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, PanelData, toLegacyResponseData } from '@grafana/ui'; import { TimeRange, LoadingState, PanelData } 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';
...@@ -32,7 +32,6 @@ export interface Props { ...@@ -32,7 +32,6 @@ export interface Props {
dashboard: DashboardModel; dashboard: DashboardModel;
plugin: PanelPlugin; plugin: PanelPlugin;
isFullscreen: boolean; isFullscreen: boolean;
isEditing: boolean;
width: number; width: number;
height: number; height: number;
} }
...@@ -50,7 +49,6 @@ export interface State { ...@@ -50,7 +49,6 @@ export interface State {
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; querySubscription: Unsubscribable;
constructor(props: Props) { constructor(props: Props) {
...@@ -64,9 +62,6 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -64,9 +62,6 @@ export class PanelChrome extends PureComponent<Props, State> {
series: [], series: [],
}, },
}; };
// Listen for changes to the query results
this.querySubscription = this.queryRunner.subscribe(this.panelDataObserver);
} }
componentDidMount() { componentDidMount() {
...@@ -91,50 +86,41 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -91,50 +86,41 @@ export class PanelChrome extends PureComponent<Props, State> {
componentWillUnmount() { componentWillUnmount() {
this.props.panel.events.off('refresh', this.onRefresh); 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 // 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 = { panelDataObserver = {
next: (data: PanelData) => { next: (data: PanelData) => {
let { errorMessage, isFirstLoad } = this.state;
if (data.state === LoadingState.Error) { if (data.state === LoadingState.Error) {
const { error } = data; const { error } = data;
if (error) { if (error) {
let message = error.message; if (this.state.errorMessage !== error.message) {
if (!message) { errorMessage = error.message;
message = 'Query error';
} }
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 { } else {
this.clearErrorState(); errorMessage = null;
} }
// Save the query response into the panel if (data.state === LoadingState.Done) {
if (data.state === LoadingState.Done && this.props.dashboard.snapshot) { // If we are doing a snapshot save data in panel model
if (this.props.dashboard.snapshot) {
this.props.panel.snapshotData = data.series; this.props.panel.snapshotData = data.series;
} }
if (this.state.isFirstLoad) {
this.setState({ data, isFirstLoad: false }); 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);
} }
this.setState({ isFirstLoad, errorMessage, data });
}, },
}; };
...@@ -153,13 +139,18 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -153,13 +139,18 @@ export class PanelChrome extends PureComponent<Props, State> {
}); });
// Issue Query // Issue Query
if (this.wantsQueryExecution && !this.hasPanelSnapshot) { if (this.wantsQueryExecution) {
if (width < 0) { if (width < 0) {
console.log('No width yet... wait till we know'); console.log('No width yet... wait till we know');
return; return;
} }
if (!panel.queryRunner) {
this.queryRunner.run({ panel.queryRunner = new PanelQueryRunner();
}
if (!this.querySubscription) {
this.querySubscription = panel.queryRunner.subscribe(this.panelDataObserver);
}
panel.queryRunner.run({
datasource: panel.datasource, datasource: panel.datasource,
queries: panel.targets, queries: panel.targets,
panelId: panel.id, panelId: panel.id,
...@@ -195,12 +186,6 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -195,12 +186,6 @@ export class PanelChrome extends PureComponent<Props, State> {
} }
}; };
clearErrorState() {
if (this.state.errorMessage) {
this.setState({ errorMessage: null });
}
}
get isVisible() { get isVisible() {
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
} }
...@@ -211,7 +196,7 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -211,7 +196,7 @@ export class PanelChrome extends PureComponent<Props, State> {
} }
get wantsQueryExecution() { 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 { renderPanel(width: number, height: number): JSX.Element {
......
...@@ -12,14 +12,16 @@ import { QueryEditorRow } from './QueryEditorRow'; ...@@ -12,14 +12,16 @@ import { QueryEditorRow } from './QueryEditorRow';
// Services // Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; 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'; import config from 'app/core/config';
// Types // Types
import { PanelModel } from '../state/PanelModel'; import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel'; 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 { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { PanelQueryRunner, PanelQueryRunnerFormat } from '../state/PanelQueryRunner';
import { Unsubscribable } from 'rxjs';
interface Props { interface Props {
panel: PanelModel; panel: PanelModel;
...@@ -33,11 +35,13 @@ interface State { ...@@ -33,11 +35,13 @@ interface State {
isPickerOpen: boolean; isPickerOpen: boolean;
isAddingMixed: boolean; isAddingMixed: boolean;
scrollTop: number; scrollTop: number;
data: PanelData;
} }
export class QueriesTab extends PureComponent<Props, State> { export class QueriesTab extends PureComponent<Props, State> {
datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources(); datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
backendSrv: BackendSrv = getBackendSrv(); backendSrv = getBackendSrv();
querySubscription: Unsubscribable;
state: State = { state: State = {
isLoadingHelp: false, isLoadingHelp: false,
...@@ -46,6 +50,40 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -46,6 +50,40 @@ export class QueriesTab extends PureComponent<Props, State> {
isPickerOpen: false, isPickerOpen: false,
isAddingMixed: false, isAddingMixed: false,
scrollTop: 0, 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 { findCurrentDataSource(): DataSourceSelectItem {
...@@ -179,7 +217,7 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -179,7 +217,7 @@ export class QueriesTab extends PureComponent<Props, State> {
render() { render() {
const { panel, dashboard } = this.props; const { panel, dashboard } = this.props;
const { currentDS, scrollTop } = this.state; const { currentDS, scrollTop, data } = this.state;
const queryInspector: EditorToolbarView = { const queryInspector: EditorToolbarView = {
title: 'Query Inspector', title: 'Query Inspector',
...@@ -208,6 +246,7 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -208,6 +246,7 @@ export class QueriesTab extends PureComponent<Props, State> {
key={query.refId} key={query.refId}
panel={panel} panel={panel}
dashboard={dashboard} dashboard={dashboard}
data={data}
query={query} query={query}
onChange={query => this.onQueryChange(query, index)} onChange={query => this.onQueryChange(query, index)}
onRemoveQuery={this.onRemoveQuery} onRemoveQuery={this.onRemoveQuery}
......
...@@ -11,11 +11,12 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; ...@@ -11,11 +11,12 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
// Types // Types
import { PanelModel } from '../state/PanelModel'; 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'; import { DashboardModel } from '../state/DashboardModel';
interface Props { interface Props {
panel: PanelModel; panel: PanelModel;
data: PanelData;
query: DataQuery; query: DataQuery;
dashboard: DashboardModel; dashboard: DashboardModel;
onAddQuery: (query?: DataQuery) => void; onAddQuery: (query?: DataQuery) => void;
...@@ -51,61 +52,14 @@ export class QueryEditorRow extends PureComponent<Props, State> { ...@@ -51,61 +52,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
componentDidMount() { componentDidMount() {
this.loadDatasource(); 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() { 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) { if (this.angularQueryEditor) {
this.angularQueryEditor.destroy(); 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 { getAngularQueryComponentScope(): AngularQueryComponentScope {
const { panel, query, dashboard } = this.props; const { panel, query, dashboard } = this.props;
const { datasource } = this.state; const { datasource } = this.state;
...@@ -134,8 +88,25 @@ export class QueryEditorRow extends PureComponent<Props, State> { ...@@ -134,8 +88,25 @@ export class QueryEditorRow extends PureComponent<Props, State> {
}); });
} }
componentDidUpdate() { componentDidUpdate(prevProps: Props) {
const { loadedDataSourceValue } = this.state; 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 // check if we need to load another datasource
if (loadedDataSourceValue !== this.props.dataSourceValue) { if (loadedDataSourceValue !== this.props.dataSourceValue) {
......
...@@ -10,6 +10,8 @@ import { DataQuery, Threshold, ScopedVars, DataQueryResponseData } from '@grafan ...@@ -10,6 +10,8 @@ import { DataQuery, Threshold, ScopedVars, DataQueryResponseData } from '@grafan
import { PanelPlugin } from 'app/types'; import { PanelPlugin } from 'app/types';
import config from 'app/core/config'; import config from 'app/core/config';
import { PanelQueryRunner } from './PanelQueryRunner';
export interface GridPos { export interface GridPos {
x: number; x: number;
y: number; y: number;
...@@ -25,6 +27,7 @@ const notPersistedProperties: { [str: string]: boolean } = { ...@@ -25,6 +27,7 @@ const notPersistedProperties: { [str: string]: boolean } = {
hasRefreshed: true, hasRefreshed: true,
cachedPluginOptions: true, cachedPluginOptions: true,
plugin: true, plugin: true,
queryRunner: true,
}; };
// For angular panels we need to clean up properties when changing type // For angular panels we need to clean up properties when changing type
...@@ -115,6 +118,7 @@ export class PanelModel { ...@@ -115,6 +118,7 @@ export class PanelModel {
cachedPluginOptions?: any; cachedPluginOptions?: any;
legend?: { show: boolean }; legend?: { show: boolean };
plugin?: PanelPlugin; plugin?: PanelPlugin;
queryRunner?: PanelQueryRunner;
constructor(model: any) { constructor(model: any) {
this.events = new Emitter(); this.events = new Emitter();
......
...@@ -39,6 +39,7 @@ export interface QueryRunnerOptions<TQuery extends DataQuery = DataQuery> { ...@@ -39,6 +39,7 @@ export interface QueryRunnerOptions<TQuery extends DataQuery = DataQuery> {
export enum PanelQueryRunnerFormat { export enum PanelQueryRunnerFormat {
series = 'series', series = 'series',
legacy = 'legacy', legacy = 'legacy',
both = 'both',
} }
export class PanelQueryRunner { export class PanelQueryRunner {
...@@ -63,6 +64,9 @@ export class PanelQueryRunner { ...@@ -63,6 +64,9 @@ export class PanelQueryRunner {
if (format === PanelQueryRunnerFormat.legacy) { if (format === PanelQueryRunnerFormat.legacy) {
this.sendLegacy = true; this.sendLegacy = true;
} else if (format === PanelQueryRunnerFormat.both) {
this.sendSeries = true;
this.sendLegacy = true;
} else { } else {
this.sendSeries = true; this.sendSeries = true;
} }
...@@ -121,6 +125,8 @@ export class PanelQueryRunner { ...@@ -121,6 +125,8 @@ export class PanelQueryRunner {
return this.data; return this.data;
} }
let loadingStateTimeoutId = 0;
try { try {
const ds = options.ds ? options.ds : await getDatasourceSrv().get(datasource, request.scopedVars); const ds = options.ds ? options.ds : await getDatasourceSrv().get(datasource, request.scopedVars);
...@@ -137,18 +143,12 @@ export class PanelQueryRunner { ...@@ -137,18 +143,12 @@ export class PanelQueryRunner {
request.intervalMs = norm.intervalMs; request.intervalMs = norm.intervalMs;
// Send a loading status event on slower queries // Send a loading status event on slower queries
setTimeout(() => { loadingStateTimeoutId = window.setTimeout(() => {
if (!request.endTime) { this.publishUpdate({ state: LoadingState.Loading });
this.data = { }, delayStateNotification || 500);
...this.data,
state: LoadingState.Loading,
request,
};
this.subject.next(this.data);
}
}, delayStateNotification || 100);
const resp = await ds.query(request); const resp = await ds.query(request);
request.endTime = Date.now(); request.endTime = Date.now();
// Make sure the response is in a supported format // Make sure the response is in a supported format
...@@ -162,15 +162,16 @@ export class PanelQueryRunner { ...@@ -162,15 +162,16 @@ export class PanelQueryRunner {
}) })
: undefined; : undefined;
// The Result // Make sure the delayed loading state timeout is cleared
this.data = { clearTimeout(loadingStateTimeoutId);
// Publish the result
return this.publishUpdate({
state: LoadingState.Done, state: LoadingState.Done,
series, series,
legacy, legacy,
request, request,
}; });
this.subject.next(this.data);
return this.data;
} catch (err) { } catch (err) {
const error = err as DataQueryError; const error = err as DataQueryError;
if (!error.message) { if (!error.message) {
...@@ -187,15 +188,26 @@ export class PanelQueryRunner { ...@@ -187,15 +188,26 @@ export class PanelQueryRunner {
error.message = message; error.message = message;
} }
this.data = { // Make sure the delayed loading state timeout is cleared
...this.data, // ?? Should we keep existing data, or clear it ??? clearTimeout(loadingStateTimeoutId);
return this.publishUpdate({
state: LoadingState.Error, state: LoadingState.Error,
error: error, error: error,
});
}
}
publishUpdate(update: Partial<PanelData>): PanelData {
this.data = {
...this.data,
...update,
}; };
this.subject.next(this.data); this.subject.next(this.data);
return this.data; return this.data;
} }
}
} }
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment