diff --git a/packages/grafana-ui/src/types/data.ts b/packages/grafana-ui/src/types/data.ts index 2485fb0b652..e006ebba908 100644 --- a/packages/grafana-ui/src/types/data.ts +++ b/packages/grafana-ui/src/types/data.ts @@ -13,15 +13,23 @@ export enum FieldType { other = 'other', // Object, Array, etc } +export interface QueryResultMeta { + [key: string]: any; + + // Match the result to the query + requestId?: string; +} + export interface QueryResultBase { /** * Matches the query target refId */ refId?: string; + /** * Used by some backend datasources to communicate back info about the execution (generated sql, timing) */ - meta?: any; + meta?: QueryResultMeta; } export interface Field { diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index 726b0769dc2..13864232565 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -1,5 +1,5 @@ import { ComponentClass } from 'react'; -import { TimeRange, RawTimeRange } from './time'; +import { TimeRange } from './time'; import { PluginMeta } from './plugin'; import { TableData, TimeSeries, SeriesData } from './data'; @@ -94,7 +94,7 @@ export interface DataSourceApi { /** * Main metrics / data query action */ - query(options: DataQueryOptions): Promise; + query(options: DataQueryRequest): Promise; /** * Test & verify datasource settings & connection details @@ -220,10 +220,11 @@ export interface ScopedVars { [key: string]: ScopedVar; } -export interface DataQueryOptions { +export interface DataQueryRequest { + requestId: string; // Used to identify results and optionally cancel the request in backendSrv timezone: string; range: TimeRange; - rangeRaw: RawTimeRange; // Duplicate of results in range. will be deprecated eventually + timeInfo?: string; // The query time description (blue text in the upper right) targets: TQuery[]; panelId: number; dashboardId: number; @@ -232,13 +233,8 @@ export interface DataQueryOptions { intervalMs: number; maxDataPoints: number; scopedVars: ScopedVars; -} -/** - * Timestamps when the query starts and stops - */ -export interface DataRequestInfo extends DataQueryOptions { - timeInfo?: string; // The query time description (blue text in the upper right) + // Request Timing startTime: number; endTime?: number; } diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index da19c0040e3..daea7e2bf0d 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -1,14 +1,14 @@ import { ComponentClass } from 'react'; import { LoadingState, SeriesData } from './data'; import { TimeRange } from './time'; -import { ScopedVars, DataRequestInfo, DataQueryError, LegacyResponseData } from './datasource'; +import { ScopedVars, DataQueryRequest, DataQueryError, LegacyResponseData } from './datasource'; export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string; export interface PanelData { state: LoadingState; series: SeriesData[]; - request?: DataRequestInfo; + request?: DataQueryRequest; error?: DataQueryError; // Data format expected by Angular panels diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 7492e5f32f5..d730b86b828 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -336,5 +336,9 @@ export class PanelModel { destroy() { this.events.emit('panel-teardown'); this.events.removeAllListeners(); + + if (this.queryRunner) { + this.queryRunner.destroy(); + } } } diff --git a/public/app/features/dashboard/state/PanelQueryRunner.test.ts b/public/app/features/dashboard/state/PanelQueryRunner.test.ts index 6e341ce5da1..33db473b1d0 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.test.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.test.ts @@ -1,5 +1,5 @@ import { getProcessedSeriesData, PanelQueryRunner } from './PanelQueryRunner'; -import { PanelData, DataQueryOptions } from '@grafana/ui/src/types'; +import { PanelData, DataQueryRequest } from '@grafana/ui/src/types'; import moment from 'moment'; describe('PanelQueryRunner', () => { @@ -46,7 +46,7 @@ interface ScenarioContext { minInterval?: string; events?: PanelData[]; res?: PanelData; - queryCalledWith?: DataQueryOptions; + queryCalledWith?: DataQueryRequest; } type ScenarioFn = (ctx: ScenarioContext) => void; @@ -70,9 +70,9 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn beforeEach(async () => { setupFn(); - const ds: any = { + const datasource: any = { interval: ctx.dsInterval, - query: (options: DataQueryOptions) => { + query: (options: DataQueryRequest) => { ctx.queryCalledWith = options; return Promise.resolve(response); }, @@ -80,8 +80,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn }; const args: any = { - ds: ds as any, - datasource: '', + datasource, minInterval: ctx.minInterval, widthPixels: ctx.widthPixels, maxDataPoints: ctx.maxDataPoints, diff --git a/public/app/features/dashboard/state/PanelQueryRunner.ts b/public/app/features/dashboard/state/PanelQueryRunner.ts index c5240cb1005..b6c36dd067e 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.ts @@ -4,6 +4,7 @@ import { Subject, Unsubscribable, PartialObserver } from 'rxjs'; // Services & Utils import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { getBackendSrv } from 'app/core/services/backend_srv'; import kbn from 'app/core/utils/kbn'; import templateSrv from 'app/features/templating/template_srv'; @@ -16,7 +17,7 @@ import { DataQuery, TimeRange, ScopedVars, - DataRequestInfo, + DataQueryRequest, SeriesData, DataQueryError, toLegacyResponseData, @@ -25,8 +26,7 @@ import { } from '@grafana/ui'; export interface QueryRunnerOptions { - ds?: DataSourceApi; // if they already have the datasource, don't look it up - datasource: string | null; + datasource: string | DataSourceApi; queries: TQuery[]; panelId: number; dashboardId?: number; @@ -47,6 +47,11 @@ export enum PanelQueryRunnerFormat { both = 'both', } +let counter = 100; +function getNextRequestId() { + return 'Q' + counter++; +} + export class PanelQueryRunner { private subject?: Subject; @@ -106,12 +111,12 @@ export class PanelQueryRunner { delayStateNotification, } = options; - const request: DataRequestInfo = { + const request: DataQueryRequest = { + requestId: getNextRequestId(), timezone, panelId, dashboardId, range: timeRange, - rangeRaw: timeRange.raw, timeInfo, interval: '', intervalMs: 0, @@ -121,6 +126,8 @@ export class PanelQueryRunner { cacheTimeout, startTime: Date.now(), }; + // Deprecated + (request as any).rangeRaw = timeRange.raw; if (!queries) { return this.publishUpdate({ @@ -134,7 +141,10 @@ export class PanelQueryRunner { let loadingStateTimeoutId = 0; try { - const ds = options.ds ? options.ds : await getDatasourceSrv().get(datasource, request.scopedVars); + const ds = + datasource && (datasource as any).query + ? (datasource as DataSourceApi) + : await getDatasourceSrv().get(datasource as string, request.scopedVars); const lowerIntervalLimit = minInterval ? templateSrv.replace(minInterval, request.scopedVars) : ds.interval; const norm = kbn.calculateInterval(timeRange, widthPixels, lowerIntervalLimit); @@ -157,6 +167,11 @@ export class PanelQueryRunner { const resp = await ds.query(request); request.endTime = Date.now(); + // Make sure we send something back -- called run() w/o subscribe! + if (!(this.sendSeries || this.sendLegacy)) { + this.sendSeries = true; + } + // Make sure the response is in a supported format const series = this.sendSeries ? getProcessedSeriesData(resp.data) : []; const legacy = this.sendLegacy @@ -214,6 +229,22 @@ export class PanelQueryRunner { return this.data; } + + /** + * Called when the panel is closed + */ + destroy() { + // Tell anyone listening that we are done + if (this.subject) { + this.subject.complete(); + } + + // If there are open HTTP requests, close them + const { request } = this.data; + if (request && request.requestId) { + getBackendSrv().resolveCancelerIfExists(request.requestId); + } + } } /** diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 510d54ec35d..abbdc8ca5d3 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; import * as dateMath from 'app/core/utils/datemath'; import kbn from 'app/core/utils/kbn'; import { CloudWatchQuery } from './types'; -import { DataSourceApi } from '@grafana/ui/src/types'; +import { DataSourceApi, DataQueryRequest } from '@grafana/ui/src/types'; // import * as moment from 'moment'; export default class CloudWatchDatasource implements DataSourceApi { @@ -23,7 +23,7 @@ export default class CloudWatchDatasource implements DataSourceApi) { options = angular.copy(options); options.targets = this.expandTemplateVariable(options.targets, options.scopedVars, this.templateSrv); diff --git a/public/app/plugins/datasource/input/datasource.ts b/public/app/plugins/datasource/input/datasource.ts index 2f65de6b40e..bf78bfb6173 100644 --- a/public/app/plugins/datasource/input/datasource.ts +++ b/public/app/plugins/datasource/input/datasource.ts @@ -1,6 +1,6 @@ // Types import { - DataQueryOptions, + DataQueryRequest, SeriesData, DataQueryResponse, DataSourceApi, @@ -67,7 +67,7 @@ export class InputDatasource implements DataSourceApi { }); } - query(options: DataQueryOptions): Promise { + query(options: DataQueryRequest): Promise { const results: SeriesData[] = []; for (const query of options.targets) { if (query.hide) { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 193ebc80b5c..33e3bf3e960 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -11,7 +11,7 @@ import { makeSeriesForLogs } from 'app/core/logs_model'; // Types import { LogsStream, LogsModel } from 'app/core/logs_model'; -import { PluginMeta, DataQueryOptions } from '@grafana/ui/src/types'; +import { PluginMeta, DataQueryRequest } from '@grafana/ui/src/types'; import { LokiQuery } from './types'; export const DEFAULT_MAX_LINES = 1000; @@ -73,7 +73,7 @@ export class LokiDatasource { }; } - async query(options: DataQueryOptions) { + async query(options: DataQueryRequest) { const queryTargets = options.targets .filter(target => target.expr && !target.hide) .map(target => this.prepareQueryTarget(target, options)); diff --git a/public/app/plugins/datasource/mixed/datasource.ts b/public/app/plugins/datasource/mixed/datasource.ts index 482cd27ab7b..4f4b10dbe5b 100644 --- a/public/app/plugins/datasource/mixed/datasource.ts +++ b/public/app/plugins/datasource/mixed/datasource.ts @@ -1,13 +1,13 @@ import _ from 'lodash'; -import { DataSourceApi, DataQuery, DataQueryOptions } from '@grafana/ui'; +import { DataSourceApi, DataQuery, DataQueryRequest } from '@grafana/ui'; import DatasourceSrv from 'app/features/plugins/datasource_srv'; class MixedDatasource implements DataSourceApi { /** @ngInject */ constructor(private datasourceSrv: DatasourceSrv) {} - query(options: DataQueryOptions) { + query(options: DataQueryRequest) { const sets = _.groupBy(options.targets, 'datasource'); const promises: any = _.map(sets, (targets: DataQuery[]) => { const dsName = targets[0].datasource; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 2ecbbae27b6..575b74b2dc5 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -15,7 +15,7 @@ import { expandRecordingRules } from './language_utils'; // Types import { PromQuery } from './types'; -import { DataQueryOptions, DataSourceApi, AnnotationEvent } from '@grafana/ui/src/types'; +import { DataQueryRequest, DataSourceApi, AnnotationEvent } from '@grafana/ui/src/types'; import { ExploreUrlState } from 'app/types/explore'; export class PrometheusDatasource implements DataSourceApi { @@ -120,7 +120,7 @@ export class PrometheusDatasource implements DataSourceApi { return this.templateSrv.variableExists(target.expr); } - query(options: DataQueryOptions) { + query(options: DataQueryRequest) { const start = this.getPrometheusTime(options.range.from, false); const end = this.getPrometheusTime(options.range.to, true); diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index a00ac509c10..73b61912e96 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -3,7 +3,7 @@ import appEvents from 'app/core/app_events'; import _ from 'lodash'; import StackdriverMetricFindQuery from './StackdriverMetricFindQuery'; import { StackdriverQuery, MetricDescriptor } from './types'; -import { DataSourceApi, DataQueryOptions } from '@grafana/ui/src/types'; +import { DataSourceApi, DataQueryRequest } from '@grafana/ui/src/types'; export default class StackdriverDatasource implements DataSourceApi { id: number; @@ -108,7 +108,7 @@ export default class StackdriverDatasource implements DataSourceApi) { + async query(options: DataQueryRequest) { const result = []; const data = await this.getTimeSeries(options); if (data.results) { diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index 541517c302d..83c55520b1e 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { DataSourceApi, DataQueryOptions, TableData, TimeSeries } from '@grafana/ui'; +import { DataSourceApi, DataQueryRequest, TableData, TimeSeries } from '@grafana/ui'; import { TestDataQuery, Scenario } from './types'; type TestData = TimeSeries | TableData; @@ -16,7 +16,7 @@ export class TestDataDatasource implements DataSourceApi { this.id = instanceSettings.id; } - query(options: DataQueryOptions) { + query(options: DataQueryRequest) { const queries = _.filter(options.targets, item => { return item.hide !== true; }).map(item => { @@ -45,8 +45,11 @@ export class TestDataDatasource implements DataSourceApi { to: options.range.to.valueOf().toString(), queries: queries, }, + + // This sets up a cancel token + requestId: options.requestId, }) - .then(res => { + .then((res: any) => { const data: TestData[] = []; // Returns data in the order it was asked for. diff --git a/public/test/helpers/getQueryOptions.ts b/public/test/helpers/getQueryOptions.ts index 5e1f534f0e4..290c450b248 100644 --- a/public/test/helpers/getQueryOptions.ts +++ b/public/test/helpers/getQueryOptions.ts @@ -1,15 +1,15 @@ -import { DataQueryOptions, DataQuery } from '@grafana/ui'; +import { DataQueryRequest, DataQuery } from '@grafana/ui'; import moment from 'moment'; export function getQueryOptions( - options: Partial> -): DataQueryOptions { + options: Partial> +): DataQueryRequest { const raw = { from: 'now', to: 'now-1h' }; const range = { from: moment(), to: moment(), raw: raw }; - const defaults: DataQueryOptions = { + const defaults: DataQueryRequest = { + requestId: 'TEST', range: range, - rangeRaw: raw, targets: [], scopedVars: {}, timezone: 'browser', @@ -18,6 +18,7 @@ export function getQueryOptions( interval: '60s', intervalMs: 60000, maxDataPoints: 500, + startTime: 0, }; Object.assign(defaults, options);