actions.ts 23 KB

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