Commit 5f474c63 by Ryan McKinley Committed by GitHub

Refactor: move getQueryRunner() to PanelModel (#16679)

* move queryRunner to panelModel
* Added interval back as prop, not used yet
* PanelQueryRunner: Refactoring, added getQueryRunner to PanelModel
* PanelQueryRunner: interpolatel min interval
parent 0f1ae76e
...@@ -22,7 +22,7 @@ import { ScopedVars } from '@grafana/ui'; ...@@ -22,7 +22,7 @@ import { ScopedVars } from '@grafana/ui';
import templateSrv from 'app/features/templating/template_srv'; import templateSrv from 'app/features/templating/template_srv';
import { PanelQueryRunner, getProcessedSeriesData } from '../state/PanelQueryRunner'; import { getProcessedSeriesData } from '../state/PanelQueryRunner';
import { Unsubscribable } from 'rxjs'; import { Unsubscribable } from 'rxjs';
const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
...@@ -134,16 +134,17 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -134,16 +134,17 @@ export class PanelChrome extends PureComponent<Props, State> {
// Issue Query // Issue Query
if (this.wantsQueryExecution) { if (this.wantsQueryExecution) {
if (width < 0) { if (width < 0) {
console.log('No width yet... wait till we know'); console.log('Refresh skippted, no width yet... wait till we know');
return; return;
} }
if (!panel.queryRunner) {
panel.queryRunner = new PanelQueryRunner(); const queryRunner = panel.getQueryRunner();
}
if (!this.querySubscription) { if (!this.querySubscription) {
this.querySubscription = panel.queryRunner.subscribe(this.panelDataObserver); this.querySubscription = queryRunner.subscribe(this.panelDataObserver);
} }
panel.queryRunner.run({
queryRunner.run({
datasource: panel.datasource, datasource: panel.datasource,
queries: panel.targets, queries: panel.targets,
panelId: panel.id, panelId: panel.id,
...@@ -152,8 +153,8 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -152,8 +153,8 @@ export class PanelChrome extends PureComponent<Props, State> {
timeRange: timeData.timeRange, timeRange: timeData.timeRange,
timeInfo: timeData.timeInfo, timeInfo: timeData.timeInfo,
widthPixels: width, widthPixels: width,
minInterval: undefined, // Currently not passed in DataPanel?
maxDataPoints: panel.maxDataPoints, maxDataPoints: panel.maxDataPoints,
minInterval: panel.interval,
scopedVars: panel.scopedVars, scopedVars: panel.scopedVars,
cacheTimeout: panel.cacheTimeout, cacheTimeout: panel.cacheTimeout,
}); });
......
...@@ -20,7 +20,7 @@ import { PanelModel } from '../state/PanelModel'; ...@@ -20,7 +20,7 @@ import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel'; import { DashboardModel } from '../state/DashboardModel';
import { DataQuery, DataSourceSelectItem, PanelData, LoadingState } 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 { PanelQueryRunnerFormat } from '../state/PanelQueryRunner';
import { Unsubscribable } from 'rxjs'; import { Unsubscribable } from 'rxjs';
interface Props { interface Props {
...@@ -58,12 +58,9 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -58,12 +58,9 @@ export class QueriesTab extends PureComponent<Props, State> {
componentDidMount() { componentDidMount() {
const { panel } = this.props; const { panel } = this.props;
const queryRunner = panel.getQueryRunner();
if (!panel.queryRunner) { this.querySubscription = queryRunner.subscribe(this.panelDataObserver, PanelQueryRunnerFormat.both);
panel.queryRunner = new PanelQueryRunner();
}
this.querySubscription = panel.queryRunner.subscribe(this.panelDataObserver, PanelQueryRunnerFormat.both);
} }
componentWillUnmount() { componentWillUnmount() {
......
...@@ -118,7 +118,7 @@ export class PanelModel { ...@@ -118,7 +118,7 @@ export class PanelModel {
cachedPluginOptions?: any; cachedPluginOptions?: any;
legend?: { show: boolean }; legend?: { show: boolean };
plugin?: PanelPlugin; plugin?: PanelPlugin;
queryRunner?: PanelQueryRunner; private queryRunner?: PanelQueryRunner;
constructor(model: any) { constructor(model: any) {
this.events = new Emitter(); this.events = new Emitter();
...@@ -326,6 +326,13 @@ export class PanelModel { ...@@ -326,6 +326,13 @@ export class PanelModel {
}); });
} }
getQueryRunner(): PanelQueryRunner {
if (!this.queryRunner) {
this.queryRunner = new PanelQueryRunner();
}
return this.queryRunner;
}
destroy() { destroy() {
this.events.emit('panel-teardown'); this.events.emit('panel-teardown');
this.events.removeAllListeners(); this.events.removeAllListeners();
......
import { getProcessedSeriesData } from './PanelQueryRunner'; import { getProcessedSeriesData, PanelQueryRunner } from './PanelQueryRunner';
import { PanelData, DataQueryOptions } from '@grafana/ui/src/types';
import moment from 'moment';
describe('QueryRunner', () => { describe('PanelQueryRunner', () => {
it('converts timeseries to table skipping nulls', () => { it('converts timeseries to table skipping nulls', () => {
const input1 = { const input1 = {
target: 'Field Name', target: 'Field Name',
...@@ -35,3 +37,133 @@ describe('QueryRunner', () => { ...@@ -35,3 +37,133 @@ describe('QueryRunner', () => {
expect(getProcessedSeriesData([])).toEqual([]); expect(getProcessedSeriesData([])).toEqual([]);
}); });
}); });
interface ScenarioContext {
setup: (fn: () => void) => void;
maxDataPoints?: number | null;
widthPixels: number;
dsInterval?: string;
minInterval?: string;
events?: PanelData[];
res?: PanelData;
queryCalledWith?: DataQueryOptions;
}
type ScenarioFn = (ctx: ScenarioContext) => void;
function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn) {
describe(description, () => {
let setupFn = () => {};
const ctx: ScenarioContext = {
widthPixels: 200,
setup: (fn: () => void) => {
setupFn = fn;
},
};
let runner: PanelQueryRunner;
const response: any = {
data: [{ target: 'hello', datapoints: [] }],
};
beforeEach(async () => {
setupFn();
const ds: any = {
interval: ctx.dsInterval,
query: (options: DataQueryOptions) => {
ctx.queryCalledWith = options;
return Promise.resolve(response);
},
testDatasource: jest.fn(),
};
const args: any = {
ds: ds as any,
datasource: '',
minInterval: ctx.minInterval,
widthPixels: ctx.widthPixels,
maxDataPoints: ctx.maxDataPoints,
timeRange: {
from: moment().subtract(1, 'days'),
to: moment(),
raw: { from: '1h', to: 'now' },
},
panelId: 0,
queries: [{ refId: 'A', test: 1 }],
};
runner = new PanelQueryRunner();
runner.subscribe({
next: (data: PanelData) => {
ctx.events.push(data);
},
});
ctx.events = [];
ctx.res = await runner.run(args);
});
scenarioFn(ctx);
});
}
describe('PanelQueryRunner', () => {
describeQueryRunnerScenario('with no maxDataPoints or minInterval', ctx => {
ctx.setup(() => {
ctx.maxDataPoints = null;
ctx.widthPixels = 200;
});
it('should return data', async () => {
expect(ctx.res.error).toBeUndefined();
expect(ctx.res.series.length).toBe(1);
});
it('should use widthPixels as maxDataPoints', async () => {
expect(ctx.queryCalledWith.maxDataPoints).toBe(200);
});
it('should calculate interval based on width', async () => {
expect(ctx.queryCalledWith.interval).toBe('5m');
});
it('fast query should only publish 1 data events', async () => {
expect(ctx.events.length).toBe(1);
});
});
describeQueryRunnerScenario('with no panel min interval but datasource min interval', ctx => {
ctx.setup(() => {
ctx.widthPixels = 20000;
ctx.dsInterval = '15s';
});
it('should limit interval to data source min interval', async () => {
expect(ctx.queryCalledWith.interval).toBe('15s');
});
});
describeQueryRunnerScenario('with panel min interval and data source min interval', ctx => {
ctx.setup(() => {
ctx.widthPixels = 20000;
ctx.dsInterval = '15s';
ctx.minInterval = '30s';
});
it('should limit interval to panel min interval', async () => {
expect(ctx.queryCalledWith.interval).toBe('30s');
});
});
describeQueryRunnerScenario('with maxDataPoints', ctx => {
ctx.setup(() => {
ctx.maxDataPoints = 10;
});
it('should pass maxDataPoints if specified', async () => {
expect(ctx.queryCalledWith.maxDataPoints).toBe(10);
});
});
});
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; // Libraries
import cloneDeep from 'lodash/cloneDeep';
import { Subject, Unsubscribable, PartialObserver } from 'rxjs'; import { Subject, Unsubscribable, PartialObserver } from 'rxjs';
// Services & Utils
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import kbn from 'app/core/utils/kbn';
import templateSrv from 'app/features/templating/template_srv';
// Components & Types
import { import {
guessFieldTypes, guessFieldTypes,
toSeriesData, toSeriesData,
...@@ -16,10 +24,6 @@ import { ...@@ -16,10 +24,6 @@ import {
DataSourceApi, DataSourceApi,
} from '@grafana/ui'; } from '@grafana/ui';
import cloneDeep from 'lodash/cloneDeep';
import kbn from 'app/core/utils/kbn';
export interface QueryRunnerOptions<TQuery extends DataQuery = DataQuery> { export interface QueryRunnerOptions<TQuery extends DataQuery = DataQuery> {
ds?: DataSourceApi<TQuery>; // if they already have the datasource, don't look it up ds?: DataSourceApi<TQuery>; // if they already have the datasource, don't look it up
datasource: string | null; datasource: string | null;
...@@ -27,11 +31,11 @@ export interface QueryRunnerOptions<TQuery extends DataQuery = DataQuery> { ...@@ -27,11 +31,11 @@ export interface QueryRunnerOptions<TQuery extends DataQuery = DataQuery> {
panelId: number; panelId: number;
dashboardId?: number; dashboardId?: number;
timezone?: string; timezone?: string;
timeRange?: TimeRange; timeRange: TimeRange;
timeInfo?: string; // String description of time range for display timeInfo?: string; // String description of time range for display
widthPixels: number; widthPixels: number;
minInterval?: string; maxDataPoints: number | undefined | null;
maxDataPoints?: number; minInterval: string | undefined | null;
scopedVars?: ScopedVars; scopedVars?: ScopedVars;
cacheTimeout?: string; cacheTimeout?: string;
delayStateNotification?: number; // default 100ms. delayStateNotification?: number; // default 100ms.
...@@ -98,6 +102,7 @@ export class PanelQueryRunner { ...@@ -98,6 +102,7 @@ export class PanelQueryRunner {
widthPixels, widthPixels,
maxDataPoints, maxDataPoints,
scopedVars, scopedVars,
minInterval,
delayStateNotification, delayStateNotification,
} = options; } = options;
...@@ -118,14 +123,12 @@ export class PanelQueryRunner { ...@@ -118,14 +123,12 @@ export class PanelQueryRunner {
}; };
if (!queries) { if (!queries) {
this.data = { return this.publishUpdate({
state: LoadingState.Done, state: LoadingState.Done,
series: [], // Clear the data series: [], // Clear the data
legacy: [], legacy: [],
request, request,
}; });
this.subject.next(this.data);
return this.data;
} }
let loadingStateTimeoutId = 0; let loadingStateTimeoutId = 0;
...@@ -133,8 +136,8 @@ export class PanelQueryRunner { ...@@ -133,8 +136,8 @@ export class PanelQueryRunner {
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);
const minInterval = options.minInterval || ds.interval; const lowerIntervalLimit = minInterval ? templateSrv.replace(minInterval, request.scopedVars) : ds.interval;
const norm = kbn.calculateInterval(timeRange, widthPixels, minInterval); const norm = kbn.calculateInterval(timeRange, widthPixels, lowerIntervalLimit);
// make shallow copy of scoped vars, // make shallow copy of scoped vars,
// and add built in variables interval and interval_ms // and add built in variables interval and interval_ms
...@@ -142,6 +145,7 @@ export class PanelQueryRunner { ...@@ -142,6 +145,7 @@ export class PanelQueryRunner {
__interval: { text: norm.interval, value: norm.interval }, __interval: { text: norm.interval, value: norm.interval },
__interval_ms: { text: norm.intervalMs, value: norm.intervalMs }, __interval_ms: { text: norm.intervalMs, value: norm.intervalMs },
}); });
request.interval = norm.interval; request.interval = norm.interval;
request.intervalMs = norm.intervalMs; request.intervalMs = norm.intervalMs;
...@@ -151,7 +155,6 @@ export class PanelQueryRunner { ...@@ -151,7 +155,6 @@ export class PanelQueryRunner {
}, delayStateNotification || 500); }, delayStateNotification || 500);
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
...@@ -229,5 +232,6 @@ export function getProcessedSeriesData(results?: any[]): SeriesData[] { ...@@ -229,5 +232,6 @@ export function getProcessedSeriesData(results?: any[]): SeriesData[] {
series.push(guessFieldTypes(toSeriesData(r))); series.push(guessFieldTypes(toSeriesData(r)));
} }
} }
return series; return series;
} }
...@@ -30,7 +30,7 @@ describe('MetricsPanelCtrl', () => { ...@@ -30,7 +30,7 @@ describe('MetricsPanelCtrl', () => {
describe('and has datasource set that supports explore and user does not have access to explore', () => { describe('and has datasource set that supports explore and user does not have access to explore', () => {
it('should not return any items', () => { it('should not return any items', () => {
const ctrl = setupController({ hasAccessToExplore: false }); const ctrl = setupController({ hasAccessToExplore: false });
ctrl.datasource = { meta: { explore: true } }; ctrl.datasource = { meta: { explore: true } } as any;
expect(ctrl.getAdditionalMenuItems().length).toBe(0); expect(ctrl.getAdditionalMenuItems().length).toBe(0);
}); });
...@@ -39,7 +39,7 @@ describe('MetricsPanelCtrl', () => { ...@@ -39,7 +39,7 @@ describe('MetricsPanelCtrl', () => {
describe('and has datasource set that supports explore and user has access to explore', () => { describe('and has datasource set that supports explore and user has access to explore', () => {
it('should return one item', () => { it('should return one item', () => {
const ctrl = setupController({ hasAccessToExplore: true }); const ctrl = setupController({ hasAccessToExplore: true });
ctrl.datasource = { meta: { explore: true } }; ctrl.datasource = { meta: { explore: true } } as any;
expect(ctrl.getAdditionalMenuItems().length).toBe(1); expect(ctrl.getAdditionalMenuItems().length).toBe(1);
}); });
......
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