actions.ts 23 KB

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