actions.ts 24 KB

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