actions.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  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<void> {
  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;
  508. }
  509. if (!hasNonEmptyQuery(queries)) {
  510. dispatch(clearQueriesAction({ exploreId }));
  511. dispatch(stateSave()); // Remember to saves to state and update location
  512. return;
  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. if ((ignoreUIState || showingTable) && supportsTable) {
  520. dispatch(
  521. runQueriesForType(
  522. exploreId,
  523. 'Table',
  524. {
  525. interval,
  526. format: 'table',
  527. instant: true,
  528. valueWithRefId: true,
  529. },
  530. (data: any[]) => data[0]
  531. )
  532. );
  533. }
  534. if ((ignoreUIState || showingGraph) && supportsGraph) {
  535. dispatch(
  536. runQueriesForType(
  537. exploreId,
  538. 'Graph',
  539. {
  540. interval,
  541. format: 'time_series',
  542. instant: false,
  543. maxDataPoints: containerWidth,
  544. },
  545. makeTimeSeriesList
  546. )
  547. );
  548. }
  549. if ((ignoreUIState || showingLogs) && supportsLogs) {
  550. dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
  551. }
  552. dispatch(stateSave());
  553. };
  554. }
  555. /**
  556. * Helper action to build a query transaction object and handing the query to the datasource.
  557. * @param exploreId Explore area
  558. * @param resultType Result viewer that will be associated with this query result
  559. * @param queryOptions Query options as required by the datasource's `query()` function.
  560. * @param resultGetter Optional result extractor, e.g., if the result is a list and you only need the first element.
  561. */
  562. function runQueriesForType(
  563. exploreId: ExploreId,
  564. resultType: ResultType,
  565. queryOptions: QueryOptions,
  566. resultGetter?: ResultGetter
  567. ): ThunkResult<void> {
  568. return async (dispatch, getState) => {
  569. const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
  570. const datasourceId = datasourceInstance.meta.id;
  571. // Run all queries concurrently
  572. for (let rowIndex = 0; rowIndex < queries.length; rowIndex++) {
  573. const query = queries[rowIndex];
  574. const transaction = buildQueryTransaction(
  575. query,
  576. rowIndex,
  577. resultType,
  578. queryOptions,
  579. range,
  580. queryIntervals,
  581. scanning
  582. );
  583. dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
  584. try {
  585. const now = Date.now();
  586. const res = await datasourceInstance.query(transaction.options);
  587. eventBridge.emit('data-received', res.data || []);
  588. const latency = Date.now() - now;
  589. const { queryTransactions } = getState().explore[exploreId];
  590. const results = resultGetter ? resultGetter(res.data, transaction, queryTransactions) : res.data;
  591. dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
  592. } catch (response) {
  593. eventBridge.emit('data-error', response);
  594. dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
  595. }
  596. }
  597. };
  598. }
  599. /**
  600. * Start a scan for more results using the given scanner.
  601. * @param exploreId Explore area
  602. * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
  603. */
  604. export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
  605. return dispatch => {
  606. // Register the scanner
  607. dispatch(scanStartAction({ exploreId, scanner }));
  608. // Scanning must trigger query run, and return the new range
  609. const range = scanner();
  610. // Set the new range to be displayed
  611. dispatch(scanRangeAction({ exploreId, range }));
  612. };
  613. }
  614. /**
  615. * Reset queries to the given queries. Any modifications will be discarded.
  616. * Use this action for clicks on query examples. Triggers a query run.
  617. */
  618. export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
  619. return (dispatch, getState) => {
  620. // Inject react keys into query objects
  621. const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery(getState().explore[exploreId].queries) }));
  622. dispatch(setQueriesAction({ exploreId, queries }));
  623. dispatch(runQueries(exploreId));
  624. };
  625. }
  626. /**
  627. * Close the split view and save URL state.
  628. */
  629. export function splitClose(itemId: ExploreId): ThunkResult<void> {
  630. return dispatch => {
  631. dispatch(splitCloseAction({ itemId }));
  632. dispatch(stateSave());
  633. };
  634. }
  635. /**
  636. * Open the split view and copy the left state to be the right state.
  637. * The right state is automatically initialized.
  638. * The copy keeps all query modifications but wipes the query results.
  639. */
  640. export function splitOpen(): ThunkResult<void> {
  641. return (dispatch, getState) => {
  642. // Clone left state to become the right state
  643. const leftState = getState().explore[ExploreId.left];
  644. const queryState = getState().location.query[ExploreId.left] as string;
  645. const urlState = parseUrlState(queryState);
  646. const queryTransactions: QueryTransaction[] = [];
  647. const itemState = {
  648. ...leftState,
  649. queryTransactions,
  650. queries: leftState.queries.slice(),
  651. exploreId: ExploreId.right,
  652. urlState,
  653. };
  654. dispatch(splitOpenAction({ itemState }));
  655. dispatch(stateSave());
  656. };
  657. }
  658. const toRawTimeRange = (range: TimeRange): RawTimeRange => {
  659. let from = range.raw.from;
  660. if (isDateTime(from)) {
  661. from = from.valueOf().toString(10);
  662. }
  663. let to = range.raw.to;
  664. if (isDateTime(to)) {
  665. to = to.valueOf().toString(10);
  666. }
  667. return {
  668. from,
  669. to,
  670. };
  671. };
  672. /**
  673. * Saves Explore state to URL using the `left` and `right` parameters.
  674. * If split view is not active, `right` will not be set.
  675. */
  676. export function stateSave(): ThunkResult<void> {
  677. return (dispatch, getState) => {
  678. const { left, right, split } = getState().explore;
  679. const urlStates: { [index: string]: string } = {};
  680. const leftUrlState: ExploreUrlState = {
  681. datasource: left.datasourceInstance.name,
  682. queries: left.queries.map(clearQueryKeys),
  683. range: toRawTimeRange(left.range),
  684. ui: {
  685. showingGraph: left.showingGraph,
  686. showingLogs: left.showingLogs,
  687. showingTable: left.showingTable,
  688. dedupStrategy: left.dedupStrategy,
  689. },
  690. };
  691. urlStates.left = serializeStateToUrlParam(leftUrlState, true);
  692. if (split) {
  693. const rightUrlState: ExploreUrlState = {
  694. datasource: right.datasourceInstance.name,
  695. queries: right.queries.map(clearQueryKeys),
  696. range: toRawTimeRange(right.range),
  697. ui: {
  698. showingGraph: right.showingGraph,
  699. showingLogs: right.showingLogs,
  700. showingTable: right.showingTable,
  701. dedupStrategy: right.dedupStrategy,
  702. },
  703. };
  704. urlStates.right = serializeStateToUrlParam(rightUrlState, true);
  705. }
  706. dispatch(updateLocation({ query: urlStates }));
  707. };
  708. }
  709. /**
  710. * Creates action to collapse graph/logs/table panel. When panel is collapsed,
  711. * queries won't be run
  712. */
  713. const togglePanelActionCreator = (
  714. actionCreator:
  715. | ActionCreator<ToggleGraphPayload>
  716. | ActionCreator<ToggleLogsPayload>
  717. | ActionCreator<ToggleTablePayload>
  718. ) => (exploreId: ExploreId, isPanelVisible: boolean): ThunkResult<void> => {
  719. return dispatch => {
  720. let uiFragmentStateUpdate: Partial<ExploreUIState>;
  721. const shouldRunQueries = !isPanelVisible;
  722. switch (actionCreator.type) {
  723. case toggleGraphAction.type:
  724. uiFragmentStateUpdate = { showingGraph: !isPanelVisible };
  725. break;
  726. case toggleLogsAction.type:
  727. uiFragmentStateUpdate = { showingLogs: !isPanelVisible };
  728. break;
  729. case toggleTableAction.type:
  730. uiFragmentStateUpdate = { showingTable: !isPanelVisible };
  731. break;
  732. }
  733. dispatch(actionCreator({ exploreId }));
  734. dispatch(updateExploreUIState(exploreId, uiFragmentStateUpdate));
  735. if (shouldRunQueries) {
  736. dispatch(runQueries(exploreId));
  737. }
  738. };
  739. };
  740. /**
  741. * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
  742. */
  743. export const toggleGraph = togglePanelActionCreator(toggleGraphAction);
  744. /**
  745. * Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
  746. */
  747. export const toggleLogs = togglePanelActionCreator(toggleLogsAction);
  748. /**
  749. * Expand/collapse the table result viewer. When collapsed, table queries won't be run.
  750. */
  751. export const toggleTable = togglePanelActionCreator(toggleTableAction);
  752. /**
  753. * Change logs deduplication strategy and update URL.
  754. */
  755. export const changeDedupStrategy = (exploreId: ExploreId, dedupStrategy: LogsDedupStrategy): ThunkResult<void> => {
  756. return dispatch => {
  757. dispatch(updateExploreUIState(exploreId, { dedupStrategy }));
  758. };
  759. };
  760. export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
  761. return (dispatch, getState) => {
  762. const itemState = getState().explore[exploreId];
  763. if (!itemState.initialized) {
  764. return;
  765. }
  766. const { urlState, update, containerWidth, eventBridge } = itemState;
  767. const { datasource, queries, range: urlRange, ui } = urlState;
  768. const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
  769. const timeZone = getTimeZone(getState().user);
  770. const range = getTimeRangeFromUrl(urlRange, timeZone);
  771. // need to refresh datasource
  772. if (update.datasource) {
  773. const initialQueries = ensureQueries(queries);
  774. dispatch(initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, ui));
  775. return;
  776. }
  777. if (update.range) {
  778. dispatch(changeTimeAction({ exploreId, range }));
  779. }
  780. // need to refresh ui state
  781. if (update.ui) {
  782. dispatch(updateUIStateAction({ ...ui, exploreId }));
  783. }
  784. // need to refresh queries
  785. if (update.queries) {
  786. dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
  787. }
  788. // always run queries when refresh is needed
  789. if (update.queries || update.ui || update.range) {
  790. dispatch(runQueries(exploreId));
  791. }
  792. };
  793. }