actions.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. // Libraries
  2. // @ts-ignore
  3. import _ from 'lodash';
  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. parseUrlState,
  19. } from 'app/core/utils/explore';
  20. // Actions
  21. import { updateLocation } from 'app/core/actions';
  22. // Types
  23. import { ThunkResult } from 'app/types';
  24. import {
  25. RawTimeRange,
  26. TimeRange,
  27. DataSourceApi,
  28. DataQuery,
  29. DataSourceSelectItem,
  30. QueryHint,
  31. QueryFixAction,
  32. } from '@grafana/ui/src/types';
  33. import {
  34. ExploreId,
  35. ExploreUrlState,
  36. RangeScanner,
  37. ResultType,
  38. QueryOptions,
  39. ExploreUIState,
  40. QueryTransaction,
  41. } from 'app/types/explore';
  42. import {
  43. updateDatasourceInstanceAction,
  44. changeQueryAction,
  45. changeSizeAction,
  46. ChangeSizePayload,
  47. changeTimeAction,
  48. scanStopAction,
  49. clearQueriesAction,
  50. initializeExploreAction,
  51. loadDatasourceMissingAction,
  52. loadDatasourcePendingAction,
  53. queriesImportedAction,
  54. LoadDatasourceReadyPayload,
  55. loadDatasourceReadyAction,
  56. modifyQueriesAction,
  57. queryTransactionFailureAction,
  58. queryTransactionStartAction,
  59. queryTransactionSuccessAction,
  60. scanRangeAction,
  61. scanStartAction,
  62. setQueriesAction,
  63. splitCloseAction,
  64. splitOpenAction,
  65. addQueryRowAction,
  66. toggleGraphAction,
  67. toggleLogsAction,
  68. toggleTableAction,
  69. ToggleGraphPayload,
  70. ToggleLogsPayload,
  71. ToggleTablePayload,
  72. updateUIStateAction,
  73. runQueriesAction,
  74. testDataSourcePendingAction,
  75. testDataSourceSuccessAction,
  76. testDataSourceFailureAction,
  77. loadExploreDatasources,
  78. } from './actionTypes';
  79. import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
  80. import { LogsDedupStrategy } from 'app/core/logs_model';
  81. import { parseTime } from '../TimePicker';
  82. /**
  83. * Updates UI state and save it to the URL
  84. */
  85. const updateExploreUIState = (exploreId: ExploreId, uiStateFragment: Partial<ExploreUIState>): ThunkResult<void> => {
  86. return dispatch => {
  87. dispatch(updateUIStateAction({ exploreId, ...uiStateFragment }));
  88. dispatch(stateSave());
  89. };
  90. };
  91. /**
  92. * Adds a query row after the row with the given index.
  93. */
  94. export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
  95. return (dispatch, getState) => {
  96. const query = generateEmptyQuery(getState().explore[exploreId].queries, index);
  97. dispatch(addQueryRowAction({ exploreId, index, query }));
  98. };
  99. }
  100. /**
  101. * Loads a new datasource identified by the given name.
  102. */
  103. export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
  104. return async (dispatch, getState) => {
  105. let newDataSourceInstance: DataSourceApi = null;
  106. if (!datasource) {
  107. newDataSourceInstance = await getDatasourceSrv().get();
  108. } else {
  109. newDataSourceInstance = await getDatasourceSrv().get(datasource);
  110. }
  111. const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
  112. const queries = getState().explore[exploreId].queries;
  113. await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
  114. dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
  115. await dispatch(loadDatasource(exploreId, newDataSourceInstance));
  116. dispatch(runQueries(exploreId));
  117. };
  118. }
  119. /**
  120. * Query change handler for the query row with the given index.
  121. * If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
  122. */
  123. export function changeQuery(
  124. exploreId: ExploreId,
  125. query: DataQuery,
  126. index: number,
  127. override: boolean
  128. ): ThunkResult<void> {
  129. return (dispatch, getState) => {
  130. // Null query means reset
  131. if (query === null) {
  132. query = { ...generateEmptyQuery(getState().explore[exploreId].queries) };
  133. }
  134. dispatch(changeQueryAction({ exploreId, query, index, override }));
  135. if (override) {
  136. dispatch(runQueries(exploreId));
  137. }
  138. };
  139. }
  140. /**
  141. * Keep track of the Explore container size, in particular the width.
  142. * The width will be used to calculate graph intervals (number of datapoints).
  143. */
  144. export function changeSize(
  145. exploreId: ExploreId,
  146. { height, width }: { height: number; width: number }
  147. ): ActionOf<ChangeSizePayload> {
  148. return changeSizeAction({ exploreId, height, width });
  149. }
  150. /**
  151. * Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
  152. */
  153. export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
  154. return dispatch => {
  155. dispatch(changeTimeAction({ exploreId, range }));
  156. dispatch(runQueries(exploreId));
  157. };
  158. }
  159. /**
  160. * Clear all queries and results.
  161. */
  162. export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
  163. return dispatch => {
  164. dispatch(scanStopAction({ exploreId }));
  165. dispatch(clearQueriesAction({ exploreId }));
  166. dispatch(stateSave());
  167. };
  168. }
  169. /**
  170. * Loads all explore data sources and sets the chosen datasource.
  171. * If there are no datasources a missing datasource action is dispatched.
  172. */
  173. export function loadExploreDatasourcesAndSetDatasource(
  174. exploreId: ExploreId,
  175. datasourceName: string
  176. ): ThunkResult<void> {
  177. return dispatch => {
  178. const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
  179. .getExternal()
  180. .map((ds: any) => ({
  181. value: ds.name,
  182. name: ds.name,
  183. meta: ds.meta,
  184. }));
  185. dispatch(loadExploreDatasources({ exploreId, exploreDatasources }));
  186. if (exploreDatasources.length >= 1) {
  187. dispatch(changeDatasource(exploreId, datasourceName));
  188. } else {
  189. dispatch(loadDatasourceMissingAction({ exploreId }));
  190. }
  191. };
  192. }
  193. /**
  194. * Initialize Explore state with state from the URL and the React component.
  195. * Call this only on components for with the Explore state has not been initialized.
  196. */
  197. export function initializeExplore(
  198. exploreId: ExploreId,
  199. datasourceName: string,
  200. queries: DataQuery[],
  201. range: RawTimeRange,
  202. containerWidth: number,
  203. eventBridge: Emitter,
  204. ui: ExploreUIState
  205. ): ThunkResult<void> {
  206. return async dispatch => {
  207. dispatch(loadExploreDatasourcesAndSetDatasource(exploreId, datasourceName));
  208. dispatch(
  209. initializeExploreAction({
  210. exploreId,
  211. containerWidth,
  212. eventBridge,
  213. queries,
  214. range,
  215. ui,
  216. })
  217. );
  218. };
  219. }
  220. /**
  221. * Datasource loading was successfully completed.
  222. */
  223. export const loadDatasourceReady = (
  224. exploreId: ExploreId,
  225. instance: DataSourceApi
  226. ): ActionOf<LoadDatasourceReadyPayload> => {
  227. const historyKey = `grafana.explore.history.${instance.meta.id}`;
  228. const history = store.getObject(historyKey, []);
  229. // Save last-used datasource
  230. store.set(LAST_USED_DATASOURCE_KEY, instance.name);
  231. return loadDatasourceReadyAction({
  232. exploreId,
  233. history,
  234. });
  235. };
  236. export function importQueries(
  237. exploreId: ExploreId,
  238. queries: DataQuery[],
  239. sourceDataSource: DataSourceApi,
  240. targetDataSource: DataSourceApi
  241. ): ThunkResult<void> {
  242. return async dispatch => {
  243. if (!sourceDataSource) {
  244. // explore not initialized
  245. dispatch(queriesImportedAction({ exploreId, queries }));
  246. return;
  247. }
  248. let importedQueries = queries;
  249. // Check if queries can be imported from previously selected datasource
  250. if (sourceDataSource.meta.id === targetDataSource.meta.id) {
  251. // Keep same queries if same type of datasource
  252. importedQueries = [...queries];
  253. } else if (targetDataSource.importQueries) {
  254. // Datasource-specific importers
  255. importedQueries = await targetDataSource.importQueries(queries, sourceDataSource.meta);
  256. } else {
  257. // Default is blank queries
  258. importedQueries = ensureQueries();
  259. }
  260. const nextQueries = importedQueries.map((q, i) => ({
  261. ...q,
  262. ...generateEmptyQuery(queries),
  263. }));
  264. dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
  265. };
  266. }
  267. /**
  268. * Tests datasource.
  269. */
  270. export const testDatasource = (exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> => {
  271. return async dispatch => {
  272. let datasourceError = null;
  273. dispatch(testDataSourcePendingAction({ exploreId }));
  274. try {
  275. const testResult = await instance.testDatasource();
  276. datasourceError = testResult.status === 'success' ? null : testResult.message;
  277. } catch (error) {
  278. datasourceError = (error && error.statusText) || 'Network error';
  279. }
  280. if (datasourceError) {
  281. dispatch(testDataSourceFailureAction({ exploreId, error: datasourceError }));
  282. return;
  283. }
  284. dispatch(testDataSourceSuccessAction({ exploreId }));
  285. };
  286. };
  287. /**
  288. * Reconnects datasource when there is a connection failure.
  289. */
  290. export const reconnectDatasource = (exploreId: ExploreId): ThunkResult<void> => {
  291. return async (dispatch, getState) => {
  292. const instance = getState().explore[exploreId].datasourceInstance;
  293. dispatch(changeDatasource(exploreId, instance.name));
  294. };
  295. };
  296. /**
  297. * Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
  298. */
  299. export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> {
  300. return async (dispatch, getState) => {
  301. const datasourceName = instance.name;
  302. // Keep ID to track selection
  303. dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
  304. await dispatch(testDatasource(exploreId, instance));
  305. if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
  306. // User already changed datasource again, discard results
  307. return;
  308. }
  309. if (instance.init) {
  310. instance.init();
  311. }
  312. if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
  313. // User already changed datasource again, discard results
  314. return;
  315. }
  316. dispatch(loadDatasourceReady(exploreId, instance));
  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: QueryFixAction,
  329. index: number,
  330. modifier: any
  331. ): ThunkResult<void> {
  332. return dispatch => {
  333. dispatch(modifyQueriesAction({ 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(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions }));
  393. };
  394. }
  395. /**
  396. * Complete a query transaction, mark the transaction as `done` and store query state in URL.
  397. * If the transaction was started by a scanner, it keeps on scanning for more results.
  398. * Side-effect: the query is stored in localStorage.
  399. * @param exploreId Explore area
  400. * @param transactionId ID
  401. * @param result Response from `datasourceInstance.query()`
  402. * @param latency Duration between request and response
  403. * @param queries Queries from all query rows
  404. * @param datasourceId Origin datasource instance, used to discard results if current datasource is different
  405. */
  406. export function queryTransactionSuccess(
  407. exploreId: ExploreId,
  408. transactionId: string,
  409. result: any,
  410. latency: number,
  411. queries: DataQuery[],
  412. datasourceId: string
  413. ): ThunkResult<void> {
  414. return (dispatch, getState) => {
  415. const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId];
  416. // If datasource already changed, results do not matter
  417. if (datasourceInstance.meta.id !== datasourceId) {
  418. return;
  419. }
  420. // Transaction might have been discarded
  421. const transaction = queryTransactions.find(qt => qt.id === transactionId);
  422. if (!transaction) {
  423. return;
  424. }
  425. // Get query hints
  426. let hints: QueryHint[];
  427. if (datasourceInstance.getQueryHints) {
  428. hints = datasourceInstance.getQueryHints(transaction.query, result);
  429. }
  430. // Mark transactions as complete and attach result
  431. const nextQueryTransactions = queryTransactions.map(qt => {
  432. if (qt.id === transactionId) {
  433. return {
  434. ...qt,
  435. hints,
  436. latency,
  437. result,
  438. done: true,
  439. };
  440. }
  441. return qt;
  442. });
  443. // Side-effect: Saving history in localstorage
  444. const nextHistory = updateHistory(history, datasourceId, queries);
  445. dispatch(
  446. queryTransactionSuccessAction({
  447. exploreId,
  448. history: nextHistory,
  449. queryTransactions: nextQueryTransactions,
  450. })
  451. );
  452. // Keep scanning for results if this was the last scanning transaction
  453. if (scanning) {
  454. if (_.size(result) === 0) {
  455. const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
  456. if (!other) {
  457. const range = scanner();
  458. dispatch(scanRangeAction({ exploreId, range }));
  459. }
  460. } else {
  461. // We can stop scanning if we have a result
  462. dispatch(scanStopAction({ exploreId }));
  463. }
  464. }
  465. };
  466. }
  467. /**
  468. * Main action to run queries and dispatches sub-actions based on which result viewers are active
  469. */
  470. export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkResult<void> {
  471. return (dispatch, getState) => {
  472. const {
  473. datasourceInstance,
  474. queries,
  475. showingLogs,
  476. showingGraph,
  477. showingTable,
  478. supportsGraph,
  479. supportsLogs,
  480. supportsTable,
  481. datasourceError,
  482. } = getState().explore[exploreId];
  483. if (datasourceError) {
  484. // let's not run any queries if data source is in a faulty state
  485. return;
  486. }
  487. if (!hasNonEmptyQuery(queries)) {
  488. dispatch(clearQueriesAction({ exploreId }));
  489. dispatch(stateSave()); // Remember to saves to state and update location
  490. return;
  491. }
  492. // Some datasource's query builders allow per-query interval limits,
  493. // but we're using the datasource interval limit for now
  494. const interval = datasourceInstance.interval;
  495. dispatch(runQueriesAction());
  496. // Keep table queries first since they need to return quickly
  497. if ((ignoreUIState || showingTable) && supportsTable) {
  498. dispatch(
  499. runQueriesForType(
  500. exploreId,
  501. 'Table',
  502. {
  503. interval,
  504. format: 'table',
  505. instant: true,
  506. valueWithRefId: true,
  507. },
  508. (data: any) => data[0]
  509. )
  510. );
  511. }
  512. if ((ignoreUIState || showingGraph) && supportsGraph) {
  513. dispatch(
  514. runQueriesForType(
  515. exploreId,
  516. 'Graph',
  517. {
  518. interval,
  519. format: 'time_series',
  520. instant: false,
  521. },
  522. makeTimeSeriesList
  523. )
  524. );
  525. }
  526. if ((ignoreUIState || showingLogs) && supportsLogs) {
  527. dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
  528. }
  529. dispatch(stateSave());
  530. };
  531. }
  532. /**
  533. * Helper action to build a query transaction object and handing the query to the datasource.
  534. * @param exploreId Explore area
  535. * @param resultType Result viewer that will be associated with this query result
  536. * @param queryOptions Query options as required by the datasource's `query()` function.
  537. * @param resultGetter Optional result extractor, e.g., if the result is a list and you only need the first element.
  538. */
  539. function runQueriesForType(
  540. exploreId: ExploreId,
  541. resultType: ResultType,
  542. queryOptions: QueryOptions,
  543. resultGetter?: any
  544. ): ThunkResult<void> {
  545. return async (dispatch, getState) => {
  546. const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
  547. const datasourceId = datasourceInstance.meta.id;
  548. // Run all queries concurrently
  549. queries.forEach(async (query, rowIndex) => {
  550. const transaction = buildQueryTransaction(
  551. query,
  552. rowIndex,
  553. resultType,
  554. queryOptions,
  555. range,
  556. queryIntervals,
  557. scanning
  558. );
  559. dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
  560. try {
  561. const now = Date.now();
  562. const res = await datasourceInstance.query(transaction.options);
  563. eventBridge.emit('data-received', res.data || []);
  564. const latency = Date.now() - now;
  565. const { queryTransactions } = getState().explore[exploreId];
  566. const results = resultGetter ? resultGetter(res.data, transaction, queryTransactions) : res.data;
  567. dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
  568. } catch (response) {
  569. eventBridge.emit('data-error', response);
  570. dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
  571. }
  572. });
  573. };
  574. }
  575. /**
  576. * Start a scan for more results using the given scanner.
  577. * @param exploreId Explore area
  578. * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
  579. */
  580. export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
  581. return dispatch => {
  582. // Register the scanner
  583. dispatch(scanStartAction({ exploreId, scanner }));
  584. // Scanning must trigger query run, and return the new range
  585. const range = scanner();
  586. // Set the new range to be displayed
  587. dispatch(scanRangeAction({ exploreId, range }));
  588. };
  589. }
  590. /**
  591. * Reset queries to the given queries. Any modifications will be discarded.
  592. * Use this action for clicks on query examples. Triggers a query run.
  593. */
  594. export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
  595. return (dispatch, getState) => {
  596. // Inject react keys into query objects
  597. const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery(getState().explore[exploreId].queries) }));
  598. dispatch(setQueriesAction({ exploreId, queries }));
  599. dispatch(runQueries(exploreId));
  600. };
  601. }
  602. /**
  603. * Close the split view and save URL state.
  604. */
  605. export function splitClose(itemId: ExploreId): ThunkResult<void> {
  606. return dispatch => {
  607. dispatch(splitCloseAction({ itemId }));
  608. dispatch(stateSave());
  609. };
  610. }
  611. /**
  612. * Open the split view and copy the left state to be the right state.
  613. * The right state is automatically initialized.
  614. * The copy keeps all query modifications but wipes the query results.
  615. */
  616. export function splitOpen(): ThunkResult<void> {
  617. return (dispatch, getState) => {
  618. // Clone left state to become the right state
  619. const leftState = getState().explore[ExploreId.left];
  620. const queryState = getState().location.query[ExploreId.left] as string;
  621. const urlState = parseUrlState(queryState);
  622. const queryTransactions: QueryTransaction[] = [];
  623. const itemState = {
  624. ...leftState,
  625. queryTransactions,
  626. queries: leftState.queries.slice(),
  627. exploreId: ExploreId.right,
  628. urlState,
  629. };
  630. dispatch(splitOpenAction({ itemState }));
  631. dispatch(stateSave());
  632. };
  633. }
  634. /**
  635. * Saves Explore state to URL using the `left` and `right` parameters.
  636. * If split view is not active, `right` will not be set.
  637. */
  638. export function stateSave(): ThunkResult<void> {
  639. return (dispatch, getState) => {
  640. const { left, right, split } = getState().explore;
  641. const urlStates: { [index: string]: string } = {};
  642. const leftUrlState: ExploreUrlState = {
  643. datasource: left.datasourceInstance.name,
  644. queries: left.queries.map(clearQueryKeys),
  645. range: left.range,
  646. ui: {
  647. showingGraph: left.showingGraph,
  648. showingLogs: left.showingLogs,
  649. showingTable: left.showingTable,
  650. dedupStrategy: left.dedupStrategy,
  651. },
  652. };
  653. urlStates.left = serializeStateToUrlParam(leftUrlState, true);
  654. if (split) {
  655. const rightUrlState: ExploreUrlState = {
  656. datasource: right.datasourceInstance.name,
  657. queries: right.queries.map(clearQueryKeys),
  658. range: right.range,
  659. ui: {
  660. showingGraph: right.showingGraph,
  661. showingLogs: right.showingLogs,
  662. showingTable: right.showingTable,
  663. dedupStrategy: right.dedupStrategy,
  664. },
  665. };
  666. urlStates.right = serializeStateToUrlParam(rightUrlState, true);
  667. }
  668. dispatch(updateLocation({ query: urlStates }));
  669. };
  670. }
  671. /**
  672. * Creates action to collapse graph/logs/table panel. When panel is collapsed,
  673. * queries won't be run
  674. */
  675. const togglePanelActionCreator = (
  676. actionCreator:
  677. | ActionCreator<ToggleGraphPayload>
  678. | ActionCreator<ToggleLogsPayload>
  679. | ActionCreator<ToggleTablePayload>
  680. ) => (exploreId: ExploreId, isPanelVisible: boolean): ThunkResult<void> => {
  681. return dispatch => {
  682. let uiFragmentStateUpdate: Partial<ExploreUIState>;
  683. const shouldRunQueries = !isPanelVisible;
  684. switch (actionCreator.type) {
  685. case toggleGraphAction.type:
  686. uiFragmentStateUpdate = { showingGraph: !isPanelVisible };
  687. break;
  688. case toggleLogsAction.type:
  689. uiFragmentStateUpdate = { showingLogs: !isPanelVisible };
  690. break;
  691. case toggleTableAction.type:
  692. uiFragmentStateUpdate = { showingTable: !isPanelVisible };
  693. break;
  694. }
  695. dispatch(actionCreator({ exploreId }));
  696. dispatch(updateExploreUIState(exploreId, uiFragmentStateUpdate));
  697. if (shouldRunQueries) {
  698. dispatch(runQueries(exploreId));
  699. }
  700. };
  701. };
  702. /**
  703. * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
  704. */
  705. export const toggleGraph = togglePanelActionCreator(toggleGraphAction);
  706. /**
  707. * Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
  708. */
  709. export const toggleLogs = togglePanelActionCreator(toggleLogsAction);
  710. /**
  711. * Expand/collapse the table result viewer. When collapsed, table queries won't be run.
  712. */
  713. export const toggleTable = togglePanelActionCreator(toggleTableAction);
  714. /**
  715. * Change logs deduplication strategy and update URL.
  716. */
  717. export const changeDedupStrategy = (exploreId: ExploreId, dedupStrategy: LogsDedupStrategy): ThunkResult<void> => {
  718. return dispatch => {
  719. dispatch(updateExploreUIState(exploreId, { dedupStrategy }));
  720. };
  721. };
  722. export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
  723. return (dispatch, getState) => {
  724. const itemState = getState().explore[exploreId];
  725. if (!itemState.initialized) {
  726. return;
  727. }
  728. const { urlState, update, containerWidth, eventBridge } = itemState;
  729. const { datasource, queries, range, ui } = urlState;
  730. const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
  731. const refreshRange = { from: parseTime(range.from), to: parseTime(range.to) };
  732. // need to refresh datasource
  733. if (update.datasource) {
  734. const initialQueries = ensureQueries(queries);
  735. const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
  736. dispatch(initializeExplore(exploreId, datasource, initialQueries, initialRange, containerWidth, eventBridge, ui));
  737. return;
  738. }
  739. if (update.range) {
  740. dispatch(changeTimeAction({ exploreId, range: refreshRange as TimeRange }));
  741. }
  742. // need to refresh ui state
  743. if (update.ui) {
  744. dispatch(updateUIStateAction({ ...ui, exploreId }));
  745. }
  746. // need to refresh queries
  747. if (update.queries) {
  748. dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
  749. }
  750. // always run queries when refresh is needed
  751. if (update.queries || update.ui || update.range) {
  752. dispatch(runQueries(exploreId));
  753. }
  754. };
  755. }