mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
@@ -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> {
|
|||||||
// 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> {
|
|||||||
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';
|
|||||||
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> {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
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', () => {
|
|||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
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 {
|
|||||||
widthPixels,
|
widthPixels,
|
||||||
maxDataPoints,
|
maxDataPoints,
|
||||||
scopedVars,
|
scopedVars,
|
||||||
|
minInterval,
|
||||||
delayStateNotification,
|
delayStateNotification,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
@@ -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 {
|
|||||||
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 {
|
|||||||
__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 {
|
|||||||
}, 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[] {
|
|||||||
series.push(guessFieldTypes(toSeriesData(r)));
|
series.push(guessFieldTypes(toSeriesData(r)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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', () => {
|
|||||||
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);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user