actions.ts 22 KB

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