actions.ts 24 KB

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