From 0643dff2f6135bbb41e8701c8c05483128158409 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 17 Apr 2019 23:22:14 -0700 Subject: [PATCH] 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 --- .../dashboard/dashgrid/DashboardPanel.tsx | 5 +- .../dashboard/dashgrid/PanelChrome.tsx | 75 ++++++++----------- .../dashboard/panel_editor/QueriesTab.tsx | 47 +++++++++++- .../dashboard/panel_editor/QueryEditorRow.tsx | 69 +++++------------ .../features/dashboard/state/PanelModel.ts | 4 + .../dashboard/state/PanelQueryRunner.ts | 52 ++++++++----- 6 files changed, 131 insertions(+), 121 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index dcb78a8cdb2..cac0c573900 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -37,7 +37,7 @@ export class DashboardPanel extends PureComponent { element: HTMLElement; specialPanels = {}; - constructor(props) { + constructor(props: Props) { super(props); this.state = { @@ -150,7 +150,7 @@ export class DashboardPanel extends PureComponent { }; 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 { panel={panel} dashboard={dashboard} isFullscreen={isFullscreen} - isEditing={isEditing} width={width} height={height} /> diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index d08f298f7ad..8a8b41e8352 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -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 { timeSrv: TimeSrv = getTimeSrv(); - queryRunner = new PanelQueryRunner(); querySubscription: Unsubscribable; constructor(props: Props) { @@ -64,9 +62,6 @@ export class PanelChrome extends PureComponent { 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 { 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 { }); // 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 { } }; - 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 { } 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 { diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index dacfcc6b84d..3cfc0bb1d07 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -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 { datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources(); - backendSrv: BackendSrv = getBackendSrv(); + backendSrv = getBackendSrv(); + querySubscription: Unsubscribable; state: State = { isLoadingHelp: false, @@ -46,6 +50,40 @@ export class QueriesTab extends PureComponent { 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 { 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 { key={query.refId} panel={panel} dashboard={dashboard} + data={data} query={query} onChange={query => this.onQueryChange(query, index)} onRemoveQuery={this.onRemoveQuery} diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index 9e4cb8f86ea..06218dfcde2 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -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 { 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 { }); } - 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) { diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index f43ee538779..aa0a43dddfa 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -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(); diff --git a/public/app/features/dashboard/state/PanelQueryRunner.ts b/public/app/features/dashboard/state/PanelQueryRunner.ts index 8f98d25c849..b93d147ec19 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.ts @@ -39,6 +39,7 @@ export interface QueryRunnerOptions { 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 { + this.data = { + ...this.data, + ...update, + }; + + this.subject.next(this.data); + + return this.data; + } } /**