PanelQueryRunner.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. const applyFieldOverridesMock = jest.fn(); // needs to be first in this file
  2. import { Subject } from 'rxjs';
  3. // Importing this way to be able to spy on grafana/data
  4. import * as grafanaData from '@grafana/data';
  5. import { setDataSourceSrv, setEchoSrv } from '@grafana/runtime';
  6. import { Echo } from '../../../core/services/echo/Echo';
  7. import { DashboardModel } from '../../dashboard/state/index';
  8. import {
  9. createDashboardQueryRunner,
  10. setDashboardQueryRunnerFactory,
  11. } from './DashboardQueryRunner/DashboardQueryRunner';
  12. import { emptyResult } from './DashboardQueryRunner/utils';
  13. import { PanelQueryRunner } from './PanelQueryRunner';
  14. jest.mock('@grafana/data', () => ({
  15. __esModule: true,
  16. ...(jest.requireActual('@grafana/data') as any),
  17. applyFieldOverrides: applyFieldOverridesMock,
  18. }));
  19. jest.mock('app/core/services/backend_srv');
  20. jest.mock('app/core/config', () => ({
  21. config: { featureToggles: { transformations: true } },
  22. getConfig: () => ({
  23. featureToggles: {},
  24. }),
  25. }));
  26. const dashboardModel = new DashboardModel({
  27. panels: [{ id: 1, type: 'graph' }],
  28. });
  29. jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
  30. getDashboardSrv: () => {
  31. return {
  32. getCurrent: () => dashboardModel,
  33. };
  34. },
  35. }));
  36. interface ScenarioContext {
  37. setup: (fn: () => void) => void;
  38. // Options used in setup
  39. maxDataPoints?: number | null;
  40. dsInterval?: string;
  41. minInterval?: string;
  42. scopedVars: grafanaData.ScopedVars;
  43. // Filled in by the Scenario runner
  44. events?: grafanaData.PanelData[];
  45. res?: grafanaData.PanelData;
  46. queryCalledWith?: grafanaData.DataQueryRequest;
  47. runner: PanelQueryRunner;
  48. }
  49. type ScenarioFn = (ctx: ScenarioContext) => void;
  50. const defaultPanelConfig: grafanaData.DataConfigSource = {
  51. getFieldOverrideOptions: () => undefined,
  52. getTransformations: () => undefined,
  53. getDataSupport: () => ({ annotations: false, alertStates: false }),
  54. };
  55. function describeQueryRunnerScenario(
  56. description: string,
  57. scenarioFn: ScenarioFn,
  58. panelConfig?: grafanaData.DataConfigSource
  59. ) {
  60. describe(description, () => {
  61. let setupFn = () => {};
  62. const ctx: ScenarioContext = {
  63. maxDataPoints: 200,
  64. scopedVars: {
  65. server: { text: 'Server1', value: 'server-1' },
  66. },
  67. runner: new PanelQueryRunner(panelConfig || defaultPanelConfig),
  68. setup: (fn: () => void) => {
  69. setupFn = fn;
  70. },
  71. };
  72. const response: any = {
  73. data: [
  74. {
  75. target: 'hello',
  76. datapoints: [
  77. [1, 1000],
  78. [2, 2000],
  79. ],
  80. },
  81. ],
  82. };
  83. setDataSourceSrv({} as any);
  84. setDashboardQueryRunnerFactory(() => ({
  85. getResult: emptyResult,
  86. run: () => undefined,
  87. cancel: () => undefined,
  88. cancellations: () => new Subject<any>(),
  89. destroy: () => undefined,
  90. }));
  91. createDashboardQueryRunner({} as any);
  92. beforeEach(async () => {
  93. setEchoSrv(new Echo());
  94. setupFn();
  95. const datasource: any = {
  96. name: 'TestDB',
  97. uid: 'TestDB-uid',
  98. interval: ctx.dsInterval,
  99. query: (options: grafanaData.DataQueryRequest) => {
  100. ctx.queryCalledWith = options;
  101. return Promise.resolve(response);
  102. },
  103. getRef: () => ({ type: 'test', uid: 'TestDB-uid' }),
  104. testDatasource: jest.fn(),
  105. };
  106. const args: any = {
  107. datasource,
  108. scopedVars: ctx.scopedVars,
  109. minInterval: ctx.minInterval,
  110. maxDataPoints: ctx.maxDataPoints,
  111. timeRange: {
  112. from: grafanaData.dateTime().subtract(1, 'days'),
  113. to: grafanaData.dateTime(),
  114. raw: { from: '1d', to: 'now' },
  115. },
  116. panelId: 1,
  117. queries: [{ refId: 'A', test: 1 }],
  118. };
  119. ctx.runner = new PanelQueryRunner(panelConfig || defaultPanelConfig);
  120. ctx.runner.getData({ withTransforms: true, withFieldConfig: true }).subscribe({
  121. next: (data: grafanaData.PanelData) => {
  122. ctx.res = data;
  123. ctx.events?.push(data);
  124. },
  125. });
  126. ctx.events = [];
  127. ctx.runner.run(args);
  128. });
  129. scenarioFn(ctx);
  130. });
  131. }
  132. describe('PanelQueryRunner', () => {
  133. beforeEach(() => {
  134. jest.clearAllMocks();
  135. });
  136. describeQueryRunnerScenario('simple scenario', (ctx) => {
  137. it('should set requestId on request', async () => {
  138. expect(ctx.queryCalledWith?.requestId).toBe('Q100');
  139. });
  140. it('should set datasource uid on request', async () => {
  141. expect(ctx.queryCalledWith?.targets[0].datasource?.uid).toBe('TestDB-uid');
  142. });
  143. it('should pass scopedVars to datasource with interval props', async () => {
  144. expect(ctx.queryCalledWith?.scopedVars.server.text).toBe('Server1');
  145. expect(ctx.queryCalledWith?.scopedVars.__interval.text).toBe('5m');
  146. expect(ctx.queryCalledWith?.scopedVars.__interval_ms.text).toBe('300000');
  147. });
  148. });
  149. describeQueryRunnerScenario('with maxDataPoints', (ctx) => {
  150. ctx.setup(() => {
  151. ctx.maxDataPoints = 200;
  152. });
  153. it('should return data', async () => {
  154. expect(ctx.res?.error).toBeUndefined();
  155. expect(ctx.res?.series.length).toBe(1);
  156. });
  157. it('should use widthPixels as maxDataPoints', async () => {
  158. expect(ctx.queryCalledWith?.maxDataPoints).toBe(200);
  159. });
  160. it('should calculate interval based on width', async () => {
  161. expect(ctx.queryCalledWith?.interval).toBe('5m');
  162. });
  163. it('fast query should only publish 1 data events', async () => {
  164. expect(ctx.events?.length).toBe(1);
  165. });
  166. });
  167. describeQueryRunnerScenario('with no panel min interval but datasource min interval', (ctx) => {
  168. ctx.setup(() => {
  169. ctx.maxDataPoints = 20000;
  170. ctx.dsInterval = '15s';
  171. });
  172. it('should limit interval to data source min interval', async () => {
  173. expect(ctx.queryCalledWith?.interval).toBe('15s');
  174. });
  175. });
  176. describeQueryRunnerScenario('with panel min interval and data source min interval', (ctx) => {
  177. ctx.setup(() => {
  178. ctx.maxDataPoints = 20000;
  179. ctx.dsInterval = '15s';
  180. ctx.minInterval = '30s';
  181. });
  182. it('should limit interval to panel min interval', async () => {
  183. expect(ctx.queryCalledWith?.interval).toBe('30s');
  184. });
  185. });
  186. describeQueryRunnerScenario('with maxDataPoints', (ctx) => {
  187. ctx.setup(() => {
  188. ctx.maxDataPoints = 10;
  189. });
  190. it('should pass maxDataPoints if specified', async () => {
  191. expect(ctx.queryCalledWith?.maxDataPoints).toBe(10);
  192. });
  193. it('should use instead of width to calculate interval', async () => {
  194. expect(ctx.queryCalledWith?.interval).toBe('2h');
  195. });
  196. });
  197. describeQueryRunnerScenario(
  198. 'field overrides',
  199. (ctx) => {
  200. it('should apply when field override options are set', async () => {
  201. ctx.runner.getData({ withTransforms: true, withFieldConfig: true }).subscribe({
  202. next: (data: grafanaData.PanelData) => {
  203. return data;
  204. },
  205. });
  206. expect(applyFieldOverridesMock).toBeCalled();
  207. });
  208. },
  209. {
  210. getFieldOverrideOptions: () => ({
  211. fieldConfig: {
  212. defaults: {
  213. unit: 'm/s',
  214. },
  215. // @ts-ignore
  216. overrides: [],
  217. },
  218. replaceVariables: (v) => v,
  219. theme: grafanaData.createTheme(),
  220. }),
  221. getTransformations: () => undefined,
  222. getDataSupport: () => ({ annotations: false, alertStates: false }),
  223. }
  224. );
  225. describeQueryRunnerScenario(
  226. 'transformations',
  227. (ctx) => {
  228. it('should apply when transformations are set', async () => {
  229. const spy = jest.spyOn(grafanaData, 'transformDataFrame');
  230. spy.mockClear();
  231. ctx.runner.getData({ withTransforms: true, withFieldConfig: true }).subscribe({
  232. next: (data: grafanaData.PanelData) => {
  233. return data;
  234. },
  235. });
  236. expect(spy).toBeCalled();
  237. });
  238. },
  239. {
  240. getFieldOverrideOptions: () => undefined,
  241. // @ts-ignore
  242. getTransformations: () => [{} as unknown as grafanaData.DataTransformerConfig],
  243. getDataSupport: () => ({ annotations: false, alertStates: false }),
  244. }
  245. );
  246. describeQueryRunnerScenario(
  247. 'getData',
  248. (ctx) => {
  249. it('should not apply transformations when transform option is false', async () => {
  250. const spy = jest.spyOn(grafanaData, 'transformDataFrame');
  251. spy.mockClear();
  252. ctx.runner.getData({ withTransforms: false, withFieldConfig: true }).subscribe({
  253. next: (data: grafanaData.PanelData) => {
  254. return data;
  255. },
  256. });
  257. expect(spy).not.toBeCalled();
  258. });
  259. it('should not apply field config when applyFieldConfig option is false', async () => {
  260. ctx.runner.getData({ withFieldConfig: false, withTransforms: true }).subscribe({
  261. next: (data: grafanaData.PanelData) => {
  262. return data;
  263. },
  264. });
  265. expect(applyFieldOverridesMock).not.toBeCalled();
  266. });
  267. },
  268. {
  269. getFieldOverrideOptions: () => ({
  270. fieldConfig: {
  271. defaults: {
  272. unit: 'm/s',
  273. },
  274. // @ts-ignore
  275. overrides: [],
  276. },
  277. replaceVariables: (v) => v,
  278. theme: grafanaData.createTheme(),
  279. }),
  280. // @ts-ignore
  281. getTransformations: () => [{} as unknown as grafanaData.DataTransformerConfig],
  282. getDataSupport: () => ({ annotations: false, alertStates: false }),
  283. }
  284. );
  285. describeQueryRunnerScenario(
  286. 'getData',
  287. (ctx) => {
  288. it('should not apply transformations when transform option is false', async () => {
  289. const spy = jest.spyOn(grafanaData, 'transformDataFrame');
  290. spy.mockClear();
  291. ctx.runner.getData({ withTransforms: false, withFieldConfig: true }).subscribe({
  292. next: (data: grafanaData.PanelData) => {
  293. return data;
  294. },
  295. });
  296. expect(spy).not.toBeCalled();
  297. });
  298. it('should not apply field config when applyFieldConfig option is false', async () => {
  299. ctx.runner.getData({ withFieldConfig: false, withTransforms: true }).subscribe({
  300. next: (data: grafanaData.PanelData) => {
  301. return data;
  302. },
  303. });
  304. expect(applyFieldOverridesMock).not.toBeCalled();
  305. });
  306. },
  307. {
  308. getFieldOverrideOptions: () => ({
  309. fieldConfig: {
  310. defaults: {
  311. unit: 'm/s',
  312. },
  313. // @ts-ignore
  314. overrides: [],
  315. },
  316. replaceVariables: (v) => v,
  317. theme: grafanaData.createTheme(),
  318. }),
  319. // @ts-ignore
  320. getTransformations: () => [{}],
  321. getDataSupport: () => ({ annotations: false, alertStates: false }),
  322. }
  323. );
  324. const snapshotData: grafanaData.DataFrameDTO[] = [
  325. {
  326. fields: [
  327. { name: 'time', type: grafanaData.FieldType.time, values: [1000] },
  328. { name: 'value', type: grafanaData.FieldType.number, values: [1] },
  329. ],
  330. },
  331. ];
  332. describeQueryRunnerScenario(
  333. 'getData with snapshot data',
  334. (ctx) => {
  335. it('should return snapshotted data', async () => {
  336. ctx.runner.getData({ withTransforms: false, withFieldConfig: true }).subscribe({
  337. next: (data: grafanaData.PanelData) => {
  338. expect(data.state).toBe(grafanaData.LoadingState.Done);
  339. expect(data.series).toEqual(snapshotData);
  340. expect(data.timeRange).toEqual(grafanaData.getDefaultTimeRange());
  341. return data;
  342. },
  343. });
  344. });
  345. },
  346. {
  347. ...defaultPanelConfig,
  348. snapshotData,
  349. }
  350. );
  351. });