actions.ts 23 KB

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