actions.ts 25 KB

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