actions.ts 26 KB

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