query.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. import { EMPTY, interval, Observable, of } from 'rxjs';
  2. import { thunkTester } from 'test/core/thunk/thunkTester';
  3. import {
  4. ArrayVector,
  5. DataFrame,
  6. DataQuery,
  7. DataQueryResponse,
  8. DataSourceApi,
  9. DataSourceJsonData,
  10. DataSourceWithLogsVolumeSupport,
  11. LoadingState,
  12. MutableDataFrame,
  13. PanelData,
  14. RawTimeRange,
  15. } from '@grafana/data';
  16. import { ExploreId, ExploreItemState, StoreState, ThunkDispatch } from 'app/types';
  17. import { reducerTester } from '../../../../test/core/redux/reducerTester';
  18. import { configureStore } from '../../../store/configureStore';
  19. import { setTimeSrv } from '../../dashboard/services/TimeSrv';
  20. import { createDefaultInitialState } from './helpers';
  21. import {
  22. addQueryRowAction,
  23. addResultsToCache,
  24. cancelQueries,
  25. cancelQueriesAction,
  26. cleanLogsVolumeAction,
  27. clearCache,
  28. importQueries,
  29. queryReducer,
  30. runQueries,
  31. scanStartAction,
  32. scanStopAction,
  33. storeLogsVolumeDataProviderAction,
  34. } from './query';
  35. import { makeExplorePaneState } from './utils';
  36. import Mock = jest.Mock;
  37. const { testRange, defaultInitialState } = createDefaultInitialState();
  38. jest.mock('app/features/dashboard/services/TimeSrv', () => ({
  39. ...jest.requireActual('app/features/dashboard/services/TimeSrv'),
  40. getTimeSrv: () => ({
  41. init: jest.fn(),
  42. timeRange: jest.fn().mockReturnValue({}),
  43. }),
  44. }));
  45. jest.mock('@grafana/runtime', () => ({
  46. ...(jest.requireActual('@grafana/runtime') as unknown as object),
  47. getTemplateSrv: () => ({
  48. updateTimeRange: jest.fn(),
  49. }),
  50. }));
  51. function setupQueryResponse(state: StoreState) {
  52. (state.explore[ExploreId.left].datasourceInstance?.query as Mock).mockReturnValueOnce(
  53. of({
  54. error: { message: 'test error' },
  55. data: [
  56. new MutableDataFrame({
  57. fields: [{ name: 'test', values: new ArrayVector() }],
  58. meta: {
  59. preferredVisualisationType: 'graph',
  60. },
  61. }),
  62. ],
  63. } as DataQueryResponse)
  64. );
  65. }
  66. describe('runQueries', () => {
  67. it('should pass dataFrames to state even if there is error in response', async () => {
  68. setTimeSrv({ init() {} } as any);
  69. const { dispatch, getState } = configureStore({
  70. ...(defaultInitialState as any),
  71. });
  72. setupQueryResponse(getState());
  73. await dispatch(runQueries(ExploreId.left));
  74. expect(getState().explore[ExploreId.left].showMetrics).toBeTruthy();
  75. expect(getState().explore[ExploreId.left].graphResult).toBeDefined();
  76. });
  77. it('should modify the request-id for log-volume queries', async () => {
  78. setTimeSrv({ init() {} } as any);
  79. const { dispatch, getState } = configureStore({
  80. ...(defaultInitialState as any),
  81. });
  82. setupQueryResponse(getState());
  83. await dispatch(runQueries(ExploreId.left));
  84. const state = getState().explore[ExploreId.left];
  85. expect(state.queryResponse.request?.requestId).toBe('explore_left');
  86. const datasource = state.datasourceInstance as any as DataSourceWithLogsVolumeSupport<DataQuery>;
  87. expect(datasource.getLogsVolumeDataProvider).toBeCalledWith(
  88. expect.objectContaining({
  89. requestId: 'explore_left_log_volume',
  90. })
  91. );
  92. });
  93. it('should set state to done if query completes without emitting', async () => {
  94. setTimeSrv({ init() {} } as any);
  95. const { dispatch, getState } = configureStore({
  96. ...(defaultInitialState as any),
  97. });
  98. (getState().explore[ExploreId.left].datasourceInstance?.query as Mock).mockReturnValueOnce(EMPTY);
  99. await dispatch(runQueries(ExploreId.left));
  100. await new Promise((resolve) => setTimeout(() => resolve(''), 500));
  101. expect(getState().explore[ExploreId.left].queryResponse.state).toBe(LoadingState.Done);
  102. });
  103. });
  104. describe('running queries', () => {
  105. it('should cancel running query when cancelQueries is dispatched', async () => {
  106. const unsubscribable = interval(1000);
  107. unsubscribable.subscribe();
  108. const exploreId = ExploreId.left;
  109. const initialState = {
  110. explore: {
  111. [exploreId]: {
  112. datasourceInstance: { name: 'testDs' },
  113. initialized: true,
  114. loading: true,
  115. querySubscription: unsubscribable,
  116. queries: ['A'],
  117. range: testRange,
  118. },
  119. },
  120. user: {
  121. orgId: 'A',
  122. },
  123. };
  124. const dispatchedActions = await thunkTester(initialState)
  125. .givenThunk(cancelQueries)
  126. .whenThunkIsDispatched(exploreId);
  127. expect(dispatchedActions).toEqual([
  128. scanStopAction({ exploreId }),
  129. cancelQueriesAction({ exploreId }),
  130. storeLogsVolumeDataProviderAction({ exploreId, logsVolumeDataProvider: undefined }),
  131. cleanLogsVolumeAction({ exploreId }),
  132. ]);
  133. });
  134. });
  135. describe('importing queries', () => {
  136. describe('when importing queries between the same type of data source', () => {
  137. it('remove datasource property from all of the queries', async () => {
  138. const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
  139. ...(defaultInitialState as any),
  140. explore: {
  141. [ExploreId.left]: {
  142. ...defaultInitialState.explore[ExploreId.left],
  143. datasourceInstance: { name: 'testDs', type: 'postgres' },
  144. },
  145. },
  146. });
  147. await dispatch(
  148. importQueries(
  149. ExploreId.left,
  150. [
  151. { datasource: { type: 'postgresql' }, refId: 'refId_A' },
  152. { datasource: { type: 'postgresql' }, refId: 'refId_B' },
  153. ],
  154. { name: 'Postgres1', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
  155. { name: 'Postgres2', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>
  156. )
  157. );
  158. expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('refId', 'refId_A');
  159. expect(getState().explore[ExploreId.left].queries[1]).toHaveProperty('refId', 'refId_B');
  160. expect(getState().explore[ExploreId.left].queries[0]).not.toHaveProperty('datasource');
  161. expect(getState().explore[ExploreId.left].queries[1]).not.toHaveProperty('datasource');
  162. });
  163. });
  164. });
  165. describe('reducer', () => {
  166. describe('scanning', () => {
  167. it('should start scanning', () => {
  168. const initialState: ExploreItemState = {
  169. ...makeExplorePaneState(),
  170. scanning: false,
  171. };
  172. reducerTester<ExploreItemState>()
  173. .givenReducer(queryReducer, initialState)
  174. .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left }))
  175. .thenStateShouldEqual({
  176. ...initialState,
  177. scanning: true,
  178. });
  179. });
  180. it('should stop scanning', () => {
  181. const initialState = {
  182. ...makeExplorePaneState(),
  183. scanning: true,
  184. scanRange: {} as RawTimeRange,
  185. };
  186. reducerTester<ExploreItemState>()
  187. .givenReducer(queryReducer, initialState)
  188. .whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
  189. .thenStateShouldEqual({
  190. ...initialState,
  191. scanning: false,
  192. scanRange: undefined,
  193. });
  194. });
  195. });
  196. describe('query rows', () => {
  197. it('adds a new query row', () => {
  198. reducerTester<ExploreItemState>()
  199. .givenReducer(queryReducer, {
  200. queries: [],
  201. } as unknown as ExploreItemState)
  202. .whenActionIsDispatched(
  203. addQueryRowAction({
  204. exploreId: ExploreId.left,
  205. query: { refId: 'A', key: 'mockKey' },
  206. index: 0,
  207. })
  208. )
  209. .thenStateShouldEqual({
  210. queries: [{ refId: 'A', key: 'mockKey' }],
  211. queryKeys: ['mockKey-0'],
  212. } as unknown as ExploreItemState);
  213. });
  214. });
  215. describe('caching', () => {
  216. it('should add response to cache', async () => {
  217. const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
  218. ...(defaultInitialState as any),
  219. explore: {
  220. [ExploreId.left]: {
  221. ...defaultInitialState.explore[ExploreId.left],
  222. queryResponse: {
  223. series: [{ name: 'test name' }] as DataFrame[],
  224. state: LoadingState.Done,
  225. } as PanelData,
  226. absoluteRange: { from: 1621348027000, to: 1621348050000 },
  227. },
  228. },
  229. });
  230. await dispatch(addResultsToCache(ExploreId.left));
  231. expect(getState().explore[ExploreId.left].cache).toEqual([
  232. { key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'test name' }], state: 'Done' } },
  233. ]);
  234. });
  235. it('should not add response to cache if response is still loading', async () => {
  236. const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
  237. ...(defaultInitialState as any),
  238. explore: {
  239. [ExploreId.left]: {
  240. ...defaultInitialState.explore[ExploreId.left],
  241. queryResponse: { series: [{ name: 'test name' }] as DataFrame[], state: LoadingState.Loading } as PanelData,
  242. absoluteRange: { from: 1621348027000, to: 1621348050000 },
  243. },
  244. },
  245. });
  246. await dispatch(addResultsToCache(ExploreId.left));
  247. expect(getState().explore[ExploreId.left].cache).toEqual([]);
  248. });
  249. it('should not add duplicate response to cache', async () => {
  250. const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
  251. ...(defaultInitialState as any),
  252. explore: {
  253. [ExploreId.left]: {
  254. ...defaultInitialState.explore[ExploreId.left],
  255. queryResponse: {
  256. series: [{ name: 'test name' }] as DataFrame[],
  257. state: LoadingState.Done,
  258. } as PanelData,
  259. absoluteRange: { from: 1621348027000, to: 1621348050000 },
  260. cache: [
  261. {
  262. key: 'from=1621348027000&to=1621348050000',
  263. value: { series: [{ name: 'old test name' }], state: LoadingState.Done },
  264. },
  265. ],
  266. },
  267. },
  268. });
  269. await dispatch(addResultsToCache(ExploreId.left));
  270. expect(getState().explore[ExploreId.left].cache).toHaveLength(1);
  271. expect(getState().explore[ExploreId.left].cache).toEqual([
  272. { key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'old test name' }], state: 'Done' } },
  273. ]);
  274. });
  275. it('should clear cache', async () => {
  276. const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
  277. ...(defaultInitialState as any),
  278. explore: {
  279. [ExploreId.left]: {
  280. ...defaultInitialState.explore[ExploreId.left],
  281. cache: [
  282. {
  283. key: 'from=1621348027000&to=1621348050000',
  284. value: { series: [{ name: 'old test name' }], state: 'Done' },
  285. },
  286. ],
  287. },
  288. },
  289. });
  290. await dispatch(clearCache(ExploreId.left));
  291. expect(getState().explore[ExploreId.left].cache).toEqual([]);
  292. });
  293. });
  294. describe('log volume', () => {
  295. let dispatch: ThunkDispatch,
  296. getState: () => StoreState,
  297. unsubscribes: Function[],
  298. mockLogsVolumeDataProvider: () => Observable<DataQueryResponse>;
  299. beforeEach(() => {
  300. unsubscribes = [];
  301. mockLogsVolumeDataProvider = () => {
  302. return {
  303. subscribe: () => {
  304. const unsubscribe = jest.fn();
  305. unsubscribes.push(unsubscribe);
  306. return {
  307. unsubscribe,
  308. };
  309. },
  310. } as unknown as Observable<DataQueryResponse>;
  311. };
  312. const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
  313. ...(defaultInitialState as any),
  314. explore: {
  315. [ExploreId.left]: {
  316. ...defaultInitialState.explore[ExploreId.left],
  317. datasourceInstance: {
  318. query: jest.fn(),
  319. getRef: jest.fn(),
  320. meta: {
  321. id: 'something',
  322. },
  323. getLogsVolumeDataProvider: () => {
  324. return mockLogsVolumeDataProvider();
  325. },
  326. },
  327. },
  328. },
  329. });
  330. dispatch = store.dispatch;
  331. getState = store.getState;
  332. setupQueryResponse(getState());
  333. });
  334. it('should cancel any unfinished logs volume queries when a new query is run', async () => {
  335. await dispatch(runQueries(ExploreId.left));
  336. // first query is run automatically
  337. // loading in progress - one subscription created, not cleaned up yet
  338. expect(unsubscribes).toHaveLength(1);
  339. expect(unsubscribes[0]).not.toBeCalled();
  340. setupQueryResponse(getState());
  341. await dispatch(runQueries(ExploreId.left));
  342. // a new query is run while log volume query is not resolve yet...
  343. expect(unsubscribes[0]).toBeCalled();
  344. // first subscription is cleaned up, a new subscription is created automatically
  345. expect(unsubscribes).toHaveLength(2);
  346. expect(unsubscribes[1]).not.toBeCalled();
  347. });
  348. it('should cancel log volume query when the main query is canceled', async () => {
  349. await dispatch(runQueries(ExploreId.left));
  350. expect(unsubscribes).toHaveLength(1);
  351. expect(unsubscribes[0]).not.toBeCalled();
  352. await dispatch(cancelQueries(ExploreId.left));
  353. expect(unsubscribes).toHaveLength(1);
  354. expect(unsubscribes[0]).toBeCalled();
  355. expect(getState().explore[ExploreId.left].logsVolumeData).toBeUndefined();
  356. expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeUndefined();
  357. });
  358. it('should load logs volume after running the query', async () => {
  359. await dispatch(runQueries(ExploreId.left));
  360. expect(unsubscribes).toHaveLength(1);
  361. });
  362. it('should clean any incomplete log volume data when main query is canceled', async () => {
  363. mockLogsVolumeDataProvider = () => {
  364. return of({ state: LoadingState.Loading, error: undefined, data: [] });
  365. };
  366. await dispatch(runQueries(ExploreId.left));
  367. expect(getState().explore[ExploreId.left].logsVolumeData).toBeDefined();
  368. expect(getState().explore[ExploreId.left].logsVolumeData!.state).toBe(LoadingState.Loading);
  369. expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeDefined();
  370. await dispatch(cancelQueries(ExploreId.left));
  371. expect(getState().explore[ExploreId.left].logsVolumeData).toBeUndefined();
  372. expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeUndefined();
  373. });
  374. it('keeps complete log volume data when main query is canceled', async () => {
  375. mockLogsVolumeDataProvider = () => {
  376. return of(
  377. { state: LoadingState.Loading, error: undefined, data: [] },
  378. { state: LoadingState.Done, error: undefined, data: [{}] }
  379. );
  380. };
  381. await dispatch(runQueries(ExploreId.left));
  382. expect(getState().explore[ExploreId.left].logsVolumeData).toBeDefined();
  383. expect(getState().explore[ExploreId.left].logsVolumeData!.state).toBe(LoadingState.Done);
  384. expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeDefined();
  385. await dispatch(cancelQueries(ExploreId.left));
  386. expect(getState().explore[ExploreId.left].logsVolumeData).toBeDefined();
  387. expect(getState().explore[ExploreId.left].logsVolumeData!.state).toBe(LoadingState.Done);
  388. expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeUndefined();
  389. });
  390. });
  391. });