actions.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888
  1. // Libraries
  2. import _ from 'lodash';
  3. // Services & Utils
  4. import store from 'app/core/store';
  5. import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
  6. import { Emitter } from 'app/core/core';
  7. import {
  8. LAST_USED_DATASOURCE_KEY,
  9. clearQueryKeys,
  10. ensureQueries,
  11. generateEmptyQuery,
  12. hasNonEmptyQuery,
  13. makeTimeSeriesList,
  14. updateHistory,
  15. buildQueryTransaction,
  16. serializeStateToUrlParam,
  17. parseUrlState,
  18. getTimeRange,
  19. getTimeRangeFromUrl,
  20. } from 'app/core/utils/explore';
  21. // Actions
  22. import { updateLocation } from 'app/core/actions';
  23. // Types
  24. import { ResultGetter } from 'app/types/explore';
  25. import { ThunkResult } from 'app/types';
  26. import {
  27. RawTimeRange,
  28. DataSourceApi,
  29. DataQuery,
  30. DataSourceSelectItem,
  31. QueryHint,
  32. QueryFixAction,
  33. TimeRange,
  34. } from '@grafana/ui/src/types';
  35. import {
  36. ExploreId,
  37. ExploreUrlState,
  38. RangeScanner,
  39. ResultType,
  40. QueryOptions,
  41. ExploreUIState,
  42. QueryTransaction,
  43. } from 'app/types/explore';
  44. import {
  45. updateDatasourceInstanceAction,
  46. changeQueryAction,
  47. changeRefreshIntervalAction,
  48. ChangeRefreshIntervalPayload,
  49. changeSizeAction,
  50. ChangeSizePayload,
  51. changeTimeAction,
  52. scanStopAction,
  53. clearQueriesAction,
  54. initializeExploreAction,
  55. loadDatasourceMissingAction,
  56. loadDatasourcePendingAction,
  57. queriesImportedAction,
  58. LoadDatasourceReadyPayload,
  59. loadDatasourceReadyAction,
  60. modifyQueriesAction,
  61. queryTransactionFailureAction,
  62. queryTransactionStartAction,
  63. queryTransactionSuccessAction,
  64. scanRangeAction,
  65. scanStartAction,
  66. setQueriesAction,
  67. splitCloseAction,
  68. splitOpenAction,
  69. addQueryRowAction,
  70. toggleGraphAction,
  71. toggleLogsAction,
  72. toggleTableAction,
  73. ToggleGraphPayload,
  74. ToggleLogsPayload,
  75. ToggleTablePayload,
  76. updateUIStateAction,
  77. runQueriesAction,
  78. testDataSourcePendingAction,
  79. testDataSourceSuccessAction,
  80. testDataSourceFailureAction,
  81. loadExploreDatasources,
  82. } from './actionTypes';
  83. import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
  84. import { LogsDedupStrategy } from 'app/core/logs_model';
  85. import { getTimeZone } from 'app/features/profile/state/selectors';
  86. import { isDateTime } from '@grafana/ui/src/utils/moment_wrapper';
  87. /**
  88. * Updates UI state and save it to the URL
  89. */
  90. const updateExploreUIState = (exploreId: ExploreId, uiStateFragment: Partial<ExploreUIState>): ThunkResult<void> => {
  91. return dispatch => {
  92. dispatch(updateUIStateAction({ exploreId, ...uiStateFragment }));
  93. dispatch(stateSave());
  94. };
  95. };
  96. /**
  97. * Adds a query row after the row with the given index.
  98. */
  99. export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
  100. return (dispatch, getState) => {
  101. const query = generateEmptyQuery(getState().explore[exploreId].queries, index);
  102. dispatch(addQueryRowAction({ exploreId, index, query }));
  103. };
  104. }
  105. /**
  106. * Loads a new datasource identified by the given name.
  107. */
  108. export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
  109. return async (dispatch, getState) => {
  110. let newDataSourceInstance: DataSourceApi = null;
  111. if (!datasource) {
  112. newDataSourceInstance = await getDatasourceSrv().get();
  113. } else {
  114. newDataSourceInstance = await getDatasourceSrv().get(datasource);
  115. }
  116. const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
  117. const queries = getState().explore[exploreId].queries;
  118. await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
  119. dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
  120. await dispatch(loadDatasource(exploreId, newDataSourceInstance));
  121. dispatch(runQueries(exploreId));
  122. };
  123. }
  124. /**
  125. * Query change handler for the query row with the given index.
  126. * If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
  127. */
  128. export function changeQuery(
  129. exploreId: ExploreId,
  130. query: DataQuery,
  131. index: number,
  132. override: boolean
  133. ): ThunkResult<void> {
  134. return (dispatch, getState) => {
  135. // Null query means reset
  136. if (query === null) {
  137. query = { ...generateEmptyQuery(getState().explore[exploreId].queries) };
  138. }
  139. dispatch(changeQueryAction({ exploreId, query, index, override }));
  140. if (override) {
  141. dispatch(runQueries(exploreId));
  142. }
  143. };
  144. }
  145. /**
  146. * Keep track of the Explore container size, in particular the width.
  147. * The width will be used to calculate graph intervals (number of datapoints).
  148. */
  149. export function changeSize(
  150. exploreId: ExploreId,
  151. { height, width }: { height: number; width: number }
  152. ): ActionOf<ChangeSizePayload> {
  153. return changeSizeAction({ exploreId, height, width });
  154. }
  155. /**
  156. * Change the time range of Explore. Usually called from the Time picker or a graph interaction.
  157. */
  158. export function changeTime(exploreId: ExploreId, rawRange: RawTimeRange): ThunkResult<void> {
  159. return (dispatch, getState) => {
  160. const timeZone = getTimeZone(getState().user);
  161. const range = getTimeRange(timeZone, rawRange);
  162. dispatch(changeTimeAction({ exploreId, range }));
  163. dispatch(runQueries(exploreId));
  164. };
  165. }
  166. /**
  167. * Change the refresh interval of Explore. Called from the Refresh picker.
  168. */
  169. export function changeRefreshInterval(
  170. exploreId: ExploreId,
  171. refreshInterval: string
  172. ): ActionOf<ChangeRefreshIntervalPayload> {
  173. return changeRefreshIntervalAction({ exploreId, refreshInterval });
  174. }
  175. /**
  176. * Clear all queries and results.
  177. */
  178. export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
  179. return dispatch => {
  180. dispatch(scanStopAction({ exploreId }));
  181. dispatch(clearQueriesAction({ exploreId }));
  182. dispatch(stateSave());
  183. };
  184. }
  185. /**
  186. * Loads all explore data sources and sets the chosen datasource.
  187. * If there are no datasources a missing datasource action is dispatched.
  188. */
  189. export function loadExploreDatasourcesAndSetDatasource(
  190. exploreId: ExploreId,
  191. datasourceName: string
  192. ): ThunkResult<void> {
  193. return dispatch => {
  194. const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
  195. .getExternal()
  196. .map(
  197. (ds: any) =>
  198. ({
  199. value: ds.name,
  200. name: ds.name,
  201. meta: ds.meta,
  202. } as DataSourceSelectItem)
  203. );
  204. dispatch(loadExploreDatasources({ exploreId, exploreDatasources }));
  205. if (exploreDatasources.length >= 1) {
  206. dispatch(changeDatasource(exploreId, datasourceName));
  207. } else {
  208. dispatch(loadDatasourceMissingAction({ exploreId }));
  209. }
  210. };
  211. }
  212. /**
  213. * Initialize Explore state with state from the URL and the React component.
  214. * Call this only on components for with the Explore state has not been initialized.
  215. */
  216. export function initializeExplore(
  217. exploreId: ExploreId,
  218. datasourceName: string,
  219. queries: DataQuery[],
  220. rawRange: RawTimeRange,
  221. containerWidth: number,
  222. eventBridge: Emitter,
  223. ui: ExploreUIState
  224. ): ThunkResult<void> {
  225. return async (dispatch, getState) => {
  226. const timeZone = getTimeZone(getState().user);
  227. const range = getTimeRange(timeZone, rawRange);
  228. dispatch(loadExploreDatasourcesAndSetDatasource(exploreId, datasourceName));
  229. dispatch(
  230. initializeExploreAction({
  231. exploreId,
  232. containerWidth,
  233. eventBridge,
  234. queries,
  235. range,
  236. ui,
  237. })
  238. );
  239. };
  240. }
  241. /**
  242. * Datasource loading was successfully completed.
  243. */
  244. export const loadDatasourceReady = (
  245. exploreId: ExploreId,
  246. instance: DataSourceApi
  247. ): ActionOf<LoadDatasourceReadyPayload> => {
  248. const historyKey = `grafana.explore.history.${instance.meta.id}`;
  249. const history = store.getObject(historyKey, []);
  250. // Save last-used datasource
  251. store.set(LAST_USED_DATASOURCE_KEY, instance.name);
  252. return loadDatasourceReadyAction({
  253. exploreId,
  254. history,
  255. });
  256. };
  257. export function importQueries(
  258. exploreId: ExploreId,
  259. queries: DataQuery[],
  260. sourceDataSource: DataSourceApi,
  261. targetDataSource: DataSourceApi
  262. ): ThunkResult<void> {
  263. return async dispatch => {
  264. if (!sourceDataSource) {
  265. // explore not initialized
  266. dispatch(queriesImportedAction({ exploreId, queries }));
  267. return;
  268. }
  269. let importedQueries = queries;
  270. // Check if queries can be imported from previously selected datasource
  271. if (sourceDataSource.meta.id === targetDataSource.meta.id) {
  272. // Keep same queries if same type of datasource
  273. importedQueries = [...queries];
  274. } else if (targetDataSource.importQueries) {
  275. // Datasource-specific importers
  276. importedQueries = await targetDataSource.importQueries(queries, sourceDataSource.meta);
  277. } else {
  278. // Default is blank queries
  279. importedQueries = ensureQueries();
  280. }
  281. const nextQueries = importedQueries.map((q, i) => ({
  282. ...q,
  283. ...generateEmptyQuery(queries),
  284. }));
  285. dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
  286. };
  287. }
  288. /**
  289. * Tests datasource.
  290. */
  291. export const testDatasource = (exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> => {
  292. return async dispatch => {
  293. let datasourceError = null;
  294. dispatch(testDataSourcePendingAction({ exploreId }));
  295. try {
  296. const testResult = await instance.testDatasource();
  297. datasourceError = testResult.status === 'success' ? null : testResult.message;
  298. } catch (error) {
  299. datasourceError = (error && error.statusText) || 'Network error';
  300. }
  301. if (datasourceError) {
  302. dispatch(testDataSourceFailureAction({ exploreId, error: datasourceError }));
  303. return;
  304. }
  305. dispatch(testDataSourceSuccessAction({ exploreId }));
  306. };
  307. };
  308. /**
  309. * Reconnects datasource when there is a connection failure.
  310. */
  311. export const reconnectDatasource = (exploreId: ExploreId): ThunkResult<void> => {
  312. return async (dispatch, getState) => {
  313. const instance = getState().explore[exploreId].datasourceInstance;
  314. dispatch(changeDatasource(exploreId, instance.name));
  315. };
  316. };
  317. /**
  318. * Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
  319. */
  320. export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> {
  321. return async (dispatch, getState) => {
  322. const datasourceName = instance.name;
  323. // Keep ID to track selection
  324. dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
  325. await dispatch(testDatasource(exploreId, instance));
  326. if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
  327. // User already changed datasource again, discard results
  328. return;
  329. }
  330. if (instance.init) {
  331. instance.init();
  332. }
  333. if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
  334. // User already changed datasource again, discard results
  335. return;
  336. }
  337. dispatch(loadDatasourceReady(exploreId, instance));
  338. };
  339. }
  340. /**
  341. * Action to modify a query given a datasource-specific modifier action.
  342. * @param exploreId Explore area
  343. * @param modification Action object with a type, e.g., ADD_FILTER
  344. * @param index Optional query row index. If omitted, the modification is applied to all query rows.
  345. * @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
  346. */
  347. export function modifyQueries(
  348. exploreId: ExploreId,
  349. modification: QueryFixAction,
  350. index: number,
  351. modifier: any
  352. ): ThunkResult<void> {
  353. return dispatch => {
  354. dispatch(modifyQueriesAction({ exploreId, modification, index, modifier }));
  355. if (!modification.preventSubmit) {
  356. dispatch(runQueries(exploreId));
  357. }
  358. };
  359. }
  360. /**
  361. * Mark a query transaction as failed with an error extracted from the query response.
  362. * The transaction will be marked as `done`.
  363. */
  364. export function queryTransactionFailure(
  365. exploreId: ExploreId,
  366. transactionId: string,
  367. response: any,
  368. datasourceId: string
  369. ): ThunkResult<void> {
  370. return (dispatch, getState) => {
  371. const { datasourceInstance, queryTransactions } = getState().explore[exploreId];
  372. if (datasourceInstance.meta.id !== datasourceId || response.cancelled) {
  373. // Navigated away, queries did not matter
  374. return;
  375. }
  376. // Transaction might have been discarded
  377. if (!queryTransactions.find(qt => qt.id === transactionId)) {
  378. return;
  379. }
  380. console.error(response);
  381. let error: string;
  382. let errorDetails: string;
  383. if (response.data) {
  384. if (typeof response.data === 'string') {
  385. error = response.data;
  386. } else if (response.data.error) {
  387. error = response.data.error;
  388. if (response.data.response) {
  389. errorDetails = response.data.response;
  390. }
  391. } else {
  392. throw new Error('Could not handle error response');
  393. }
  394. } else if (response.message) {
  395. error = response.message;
  396. } else if (typeof response === 'string') {
  397. error = response;
  398. } else {
  399. error = 'Unknown error during query transaction. Please check JS console logs.';
  400. }
  401. // Mark transactions as complete
  402. const nextQueryTransactions = queryTransactions.map(qt => {
  403. if (qt.id === transactionId) {
  404. return {
  405. ...qt,
  406. error,
  407. errorDetails,
  408. done: true,
  409. };
  410. }
  411. return qt;
  412. });
  413. dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions }));
  414. };
  415. }
  416. /**
  417. * Complete a query transaction, mark the transaction as `done` and store query state in URL.
  418. * If the transaction was started by a scanner, it keeps on scanning for more results.
  419. * Side-effect: the query is stored in localStorage.
  420. * @param exploreId Explore area
  421. * @param transactionId ID
  422. * @param result Response from `datasourceInstance.query()`
  423. * @param latency Duration between request and response
  424. * @param queries Queries from all query rows
  425. * @param datasourceId Origin datasource instance, used to discard results if current datasource is different
  426. */
  427. export function queryTransactionSuccess(
  428. exploreId: ExploreId,
  429. transactionId: string,
  430. result: any,
  431. latency: number,
  432. queries: DataQuery[],
  433. datasourceId: string
  434. ): ThunkResult<void> {
  435. return (dispatch, getState) => {
  436. const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId];
  437. // If datasource already changed, results do not matter
  438. if (datasourceInstance.meta.id !== datasourceId) {
  439. return;
  440. }
  441. // Transaction might have been discarded
  442. const transaction = queryTransactions.find(qt => qt.id === transactionId);
  443. if (!transaction) {
  444. return;
  445. }
  446. // Get query hints
  447. let hints: QueryHint[];
  448. if (datasourceInstance.getQueryHints) {
  449. hints = datasourceInstance.getQueryHints(transaction.query, result);
  450. }
  451. // Mark transactions as complete and attach result
  452. const nextQueryTransactions = queryTransactions.map(qt => {
  453. if (qt.id === transactionId) {
  454. return {
  455. ...qt,
  456. hints,
  457. latency,
  458. result,
  459. done: true,
  460. };
  461. }
  462. return qt;
  463. });
  464. // Side-effect: Saving history in localstorage
  465. const nextHistory = updateHistory(history, datasourceId, queries);
  466. dispatch(
  467. queryTransactionSuccessAction({
  468. exploreId,
  469. history: nextHistory,
  470. queryTransactions: nextQueryTransactions,
  471. })
  472. );
  473. // Keep scanning for results if this was the last scanning transaction
  474. if (scanning) {
  475. if (_.size(result) === 0) {
  476. const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
  477. if (!other) {
  478. const range = scanner();
  479. dispatch(scanRangeAction({ exploreId, range }));
  480. }
  481. } else {
  482. // We can stop scanning if we have a result
  483. dispatch(scanStopAction({ exploreId }));
  484. }
  485. }
  486. };
  487. }
  488. /**
  489. * Main action to run queries and dispatches sub-actions based on which result viewers are active
  490. */
  491. export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkResult<Promise<any>> {
  492. return (dispatch, getState) => {
  493. const {
  494. datasourceInstance,
  495. queries,
  496. showingLogs,
  497. showingGraph,
  498. showingTable,
  499. supportsGraph,
  500. supportsLogs,
  501. supportsTable,
  502. datasourceError,
  503. containerWidth,
  504. } = getState().explore[exploreId];
  505. if (datasourceError) {
  506. // let's not run any queries if data source is in a faulty state
  507. return Promise.resolve();
  508. }
  509. if (!hasNonEmptyQuery(queries)) {
  510. dispatch(clearQueriesAction({ exploreId }));
  511. dispatch(stateSave()); // Remember to saves to state and update location
  512. return Promise.resolve();
  513. }
  514. // Some datasource's query builders allow per-query interval limits,
  515. // but we're using the datasource interval limit for now
  516. const interval = datasourceInstance.interval;
  517. dispatch(runQueriesAction({ exploreId }));
  518. // Keep table queries first since they need to return quickly
  519. const tableQueriesPromise =
  520. (ignoreUIState || showingTable) && supportsTable
  521. ? dispatch(
  522. runQueriesForType(
  523. exploreId,
  524. 'Table',
  525. {
  526. interval,
  527. format: 'table',
  528. instant: true,
  529. valueWithRefId: true,
  530. },
  531. (data: any[]) => data[0]
  532. )
  533. )
  534. : undefined;
  535. const typeQueriesPromise =
  536. (ignoreUIState || showingGraph) && supportsGraph
  537. ? dispatch(
  538. runQueriesForType(
  539. exploreId,
  540. 'Graph',
  541. {
  542. interval,
  543. format: 'time_series',
  544. instant: false,
  545. maxDataPoints: containerWidth,
  546. },
  547. makeTimeSeriesList
  548. )
  549. )
  550. : undefined;
  551. const logsQueriesPromise =
  552. (ignoreUIState || showingLogs) && supportsLogs
  553. ? dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }))
  554. : undefined;
  555. dispatch(stateSave());
  556. return Promise.all([tableQueriesPromise, typeQueriesPromise, logsQueriesPromise]);
  557. };
  558. }
  559. /**
  560. * Helper action to build a query transaction object and handing the query to the datasource.
  561. * @param exploreId Explore area
  562. * @param resultType Result viewer that will be associated with this query result
  563. * @param queryOptions Query options as required by the datasource's `query()` function.
  564. * @param resultGetter Optional result extractor, e.g., if the result is a list and you only need the first element.
  565. */
  566. function runQueriesForType(
  567. exploreId: ExploreId,
  568. resultType: ResultType,
  569. queryOptions: QueryOptions,
  570. resultGetter?: ResultGetter
  571. ): ThunkResult<void> {
  572. return async (dispatch, getState) => {
  573. const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
  574. const datasourceId = datasourceInstance.meta.id;
  575. // Run all queries concurrently
  576. const queryPromises = queries.map(async (query, rowIndex) => {
  577. const transaction = buildQueryTransaction(
  578. query,
  579. rowIndex,
  580. resultType,
  581. queryOptions,
  582. range,
  583. queryIntervals,
  584. scanning
  585. );
  586. dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
  587. try {
  588. const now = Date.now();
  589. const res = await datasourceInstance.query(transaction.options);
  590. eventBridge.emit('data-received', res.data || []);
  591. const latency = Date.now() - now;
  592. const { queryTransactions } = getState().explore[exploreId];
  593. const results = resultGetter ? resultGetter(res.data, transaction, queryTransactions) : res.data;
  594. dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
  595. } catch (response) {
  596. eventBridge.emit('data-error', response);
  597. dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
  598. }
  599. });
  600. return Promise.all(queryPromises);
  601. };
  602. }
  603. /**
  604. * Start a scan for more results using the given scanner.
  605. * @param exploreId Explore area
  606. * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
  607. */
  608. export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
  609. return dispatch => {
  610. // Register the scanner
  611. dispatch(scanStartAction({ exploreId, scanner }));
  612. // Scanning must trigger query run, and return the new range
  613. const range = scanner();
  614. // Set the new range to be displayed
  615. dispatch(scanRangeAction({ exploreId, range }));
  616. };
  617. }
  618. /**
  619. * Reset queries to the given queries. Any modifications will be discarded.
  620. * Use this action for clicks on query examples. Triggers a query run.
  621. */
  622. export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
  623. return (dispatch, getState) => {
  624. // Inject react keys into query objects
  625. const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery(getState().explore[exploreId].queries) }));
  626. dispatch(setQueriesAction({ exploreId, queries }));
  627. dispatch(runQueries(exploreId));
  628. };
  629. }
  630. /**
  631. * Close the split view and save URL state.
  632. */
  633. export function splitClose(itemId: ExploreId): ThunkResult<void> {
  634. return dispatch => {
  635. dispatch(splitCloseAction({ itemId }));
  636. dispatch(stateSave());
  637. };
  638. }
  639. /**
  640. * Open the split view and copy the left state to be the right state.
  641. * The right state is automatically initialized.
  642. * The copy keeps all query modifications but wipes the query results.
  643. */
  644. export function splitOpen(): ThunkResult<void> {
  645. return (dispatch, getState) => {
  646. // Clone left state to become the right state
  647. const leftState = getState().explore[ExploreId.left];
  648. const queryState = getState().location.query[ExploreId.left] as string;
  649. const urlState = parseUrlState(queryState);
  650. const queryTransactions: QueryTransaction[] = [];
  651. const itemState = {
  652. ...leftState,
  653. queryTransactions,
  654. queries: leftState.queries.slice(),
  655. exploreId: ExploreId.right,
  656. urlState,
  657. };
  658. dispatch(splitOpenAction({ itemState }));
  659. dispatch(stateSave());
  660. };
  661. }
  662. const toRawTimeRange = (range: TimeRange): RawTimeRange => {
  663. let from = range.raw.from;
  664. if (isDateTime(from)) {
  665. from = from.valueOf().toString(10);
  666. }
  667. let to = range.raw.to;
  668. if (isDateTime(to)) {
  669. to = to.valueOf().toString(10);
  670. }
  671. return {
  672. from,
  673. to,
  674. };
  675. };
  676. /**
  677. * Saves Explore state to URL using the `left` and `right` parameters.
  678. * If split view is not active, `right` will not be set.
  679. */
  680. export function stateSave(): ThunkResult<void> {
  681. return (dispatch, getState) => {
  682. const { left, right, split } = getState().explore;
  683. const urlStates: { [index: string]: string } = {};
  684. const leftUrlState: ExploreUrlState = {
  685. datasource: left.datasourceInstance.name,
  686. queries: left.queries.map(clearQueryKeys),
  687. range: toRawTimeRange(left.range),
  688. ui: {
  689. showingGraph: left.showingGraph,
  690. showingLogs: left.showingLogs,
  691. showingTable: left.showingTable,
  692. dedupStrategy: left.dedupStrategy,
  693. },
  694. };
  695. urlStates.left = serializeStateToUrlParam(leftUrlState, true);
  696. if (split) {
  697. const rightUrlState: ExploreUrlState = {
  698. datasource: right.datasourceInstance.name,
  699. queries: right.queries.map(clearQueryKeys),
  700. range: toRawTimeRange(right.range),
  701. ui: {
  702. showingGraph: right.showingGraph,
  703. showingLogs: right.showingLogs,
  704. showingTable: right.showingTable,
  705. dedupStrategy: right.dedupStrategy,
  706. },
  707. };
  708. urlStates.right = serializeStateToUrlParam(rightUrlState, true);
  709. }
  710. dispatch(updateLocation({ query: urlStates }));
  711. };
  712. }
  713. /**
  714. * Creates action to collapse graph/logs/table panel. When panel is collapsed,
  715. * queries won't be run
  716. */
  717. const togglePanelActionCreator = (
  718. actionCreator:
  719. | ActionCreator<ToggleGraphPayload>
  720. | ActionCreator<ToggleLogsPayload>
  721. | ActionCreator<ToggleTablePayload>
  722. ) => (exploreId: ExploreId, isPanelVisible: boolean): ThunkResult<void> => {
  723. return dispatch => {
  724. let uiFragmentStateUpdate: Partial<ExploreUIState>;
  725. const shouldRunQueries = !isPanelVisible;
  726. switch (actionCreator.type) {
  727. case toggleGraphAction.type:
  728. uiFragmentStateUpdate = { showingGraph: !isPanelVisible };
  729. break;
  730. case toggleLogsAction.type:
  731. uiFragmentStateUpdate = { showingLogs: !isPanelVisible };
  732. break;
  733. case toggleTableAction.type:
  734. uiFragmentStateUpdate = { showingTable: !isPanelVisible };
  735. break;
  736. }
  737. dispatch(actionCreator({ exploreId }));
  738. dispatch(updateExploreUIState(exploreId, uiFragmentStateUpdate));
  739. if (shouldRunQueries) {
  740. dispatch(runQueries(exploreId));
  741. }
  742. };
  743. };
  744. /**
  745. * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
  746. */
  747. export const toggleGraph = togglePanelActionCreator(toggleGraphAction);
  748. /**
  749. * Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
  750. */
  751. export const toggleLogs = togglePanelActionCreator(toggleLogsAction);
  752. /**
  753. * Expand/collapse the table result viewer. When collapsed, table queries won't be run.
  754. */
  755. export const toggleTable = togglePanelActionCreator(toggleTableAction);
  756. /**
  757. * Change logs deduplication strategy and update URL.
  758. */
  759. export const changeDedupStrategy = (exploreId: ExploreId, dedupStrategy: LogsDedupStrategy): ThunkResult<void> => {
  760. return dispatch => {
  761. dispatch(updateExploreUIState(exploreId, { dedupStrategy }));
  762. };
  763. };
  764. export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
  765. return (dispatch, getState) => {
  766. const itemState = getState().explore[exploreId];
  767. if (!itemState.initialized) {
  768. return;
  769. }
  770. const { urlState, update, containerWidth, eventBridge } = itemState;
  771. const { datasource, queries, range: urlRange, ui } = urlState;
  772. const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
  773. const timeZone = getTimeZone(getState().user);
  774. const range = getTimeRangeFromUrl(urlRange, timeZone);
  775. // need to refresh datasource
  776. if (update.datasource) {
  777. const initialQueries = ensureQueries(queries);
  778. dispatch(initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, ui));
  779. return;
  780. }
  781. if (update.range) {
  782. dispatch(changeTimeAction({ exploreId, range }));
  783. }
  784. // need to refresh ui state
  785. if (update.ui) {
  786. dispatch(updateUIStateAction({ ...ui, exploreId }));
  787. }
  788. // need to refresh queries
  789. if (update.queries) {
  790. dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
  791. }
  792. // always run queries when refresh is needed
  793. if (update.queries || update.ui || update.range) {
  794. dispatch(runQueries(exploreId));
  795. }
  796. };
  797. }