actions.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  1. // Libraries
  2. // Services & Utils
  3. import store from 'app/core/store';
  4. import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
  5. import { Emitter } from 'app/core/core';
  6. import {
  7. ensureQueries,
  8. generateEmptyQuery,
  9. parseUrlState,
  10. getTimeRange,
  11. getTimeRangeFromUrl,
  12. generateNewKeyAndAddRefIdIfMissing,
  13. lastUsedDatasourceKeyForOrgId,
  14. hasNonEmptyQuery,
  15. buildQueryTransaction,
  16. clearQueryKeys,
  17. serializeStateToUrlParam,
  18. stopQueryState,
  19. updateHistory,
  20. } from 'app/core/utils/explore';
  21. // Types
  22. import { ThunkResult, ExploreUrlState, ExploreItemState } from 'app/types';
  23. import { DataSourceApi, DataQuery, DataSourceSelectItem, QueryFixAction, PanelData } from '@grafana/ui';
  24. import {
  25. RawTimeRange,
  26. LogsDedupStrategy,
  27. AbsoluteTimeRange,
  28. LoadingState,
  29. TimeRange,
  30. isDateTime,
  31. dateTimeForTimeZone,
  32. } from '@grafana/data';
  33. import { ExploreId, ExploreUIState, ExploreMode } from 'app/types/explore';
  34. import {
  35. updateDatasourceInstanceAction,
  36. changeQueryAction,
  37. changeRefreshIntervalAction,
  38. ChangeRefreshIntervalPayload,
  39. changeSizeAction,
  40. ChangeSizePayload,
  41. clearQueriesAction,
  42. initializeExploreAction,
  43. loadDatasourceMissingAction,
  44. loadDatasourcePendingAction,
  45. queriesImportedAction,
  46. LoadDatasourceReadyPayload,
  47. loadDatasourceReadyAction,
  48. modifyQueriesAction,
  49. scanStartAction,
  50. setQueriesAction,
  51. splitCloseAction,
  52. splitOpenAction,
  53. addQueryRowAction,
  54. toggleGraphAction,
  55. toggleTableAction,
  56. ToggleGraphPayload,
  57. ToggleTablePayload,
  58. updateUIStateAction,
  59. testDataSourcePendingAction,
  60. testDataSourceSuccessAction,
  61. testDataSourceFailureAction,
  62. loadExploreDatasources,
  63. changeModeAction,
  64. scanStopAction,
  65. queryStartAction,
  66. setUrlReplacedAction,
  67. changeRangeAction,
  68. historyUpdatedAction,
  69. queryEndedAction,
  70. queryStreamUpdatedAction,
  71. clearOriginAction,
  72. } from './actionTypes';
  73. import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
  74. import { getTimeZone } from 'app/features/profile/state/selectors';
  75. import { offOption } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
  76. import { getShiftedTimeRange } from 'app/core/utils/timePicker';
  77. import _ from 'lodash';
  78. import { updateLocation } from '../../../core/actions';
  79. import { getTimeSrv } from '../../dashboard/services/TimeSrv';
  80. /**
  81. * Updates UI state and save it to the URL
  82. */
  83. const updateExploreUIState = (exploreId: ExploreId, uiStateFragment: Partial<ExploreUIState>): ThunkResult<void> => {
  84. return dispatch => {
  85. dispatch(updateUIStateAction({ exploreId, ...uiStateFragment }));
  86. dispatch(stateSave());
  87. };
  88. };
  89. /**
  90. * Adds a query row after the row with the given index.
  91. */
  92. export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
  93. return (dispatch, getState) => {
  94. const queries = getState().explore[exploreId].queries;
  95. const query = generateEmptyQuery(queries, index);
  96. dispatch(addQueryRowAction({ exploreId, index, query }));
  97. };
  98. }
  99. /**
  100. * Loads a new datasource identified by the given name.
  101. */
  102. export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
  103. return async (dispatch, getState) => {
  104. let newDataSourceInstance: DataSourceApi = null;
  105. if (!datasource) {
  106. newDataSourceInstance = await getDatasourceSrv().get();
  107. } else {
  108. newDataSourceInstance = await getDatasourceSrv().get(datasource);
  109. }
  110. const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
  111. const queries = getState().explore[exploreId].queries;
  112. const orgId = getState().user.orgId;
  113. dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
  114. await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
  115. if (getState().explore[exploreId].isLive) {
  116. dispatch(changeRefreshInterval(exploreId, offOption.value));
  117. }
  118. await dispatch(loadDatasource(exploreId, newDataSourceInstance, orgId));
  119. dispatch(runQueries(exploreId));
  120. };
  121. }
  122. /**
  123. * Change the display mode in Explore.
  124. */
  125. export function changeMode(exploreId: ExploreId, mode: ExploreMode): ThunkResult<void> {
  126. return dispatch => {
  127. dispatch(clearQueriesAction({ exploreId }));
  128. dispatch(changeModeAction({ exploreId, mode }));
  129. };
  130. }
  131. /**
  132. * Query change handler for the query row with the given index.
  133. * If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
  134. */
  135. export function changeQuery(
  136. exploreId: ExploreId,
  137. query: DataQuery,
  138. index: number,
  139. override: boolean
  140. ): ThunkResult<void> {
  141. return (dispatch, getState) => {
  142. // Null query means reset
  143. if (query === null) {
  144. const queries = getState().explore[exploreId].queries;
  145. const { refId, key } = queries[index];
  146. query = generateNewKeyAndAddRefIdIfMissing({ refId, key }, queries, index);
  147. }
  148. dispatch(changeQueryAction({ exploreId, query, index, override }));
  149. if (override) {
  150. dispatch(runQueries(exploreId));
  151. }
  152. };
  153. }
  154. /**
  155. * Keep track of the Explore container size, in particular the width.
  156. * The width will be used to calculate graph intervals (number of datapoints).
  157. */
  158. export function changeSize(
  159. exploreId: ExploreId,
  160. { height, width }: { height: number; width: number }
  161. ): ActionOf<ChangeSizePayload> {
  162. return changeSizeAction({ exploreId, height, width });
  163. }
  164. export const updateTimeRange = (options: {
  165. exploreId: ExploreId;
  166. rawRange?: RawTimeRange;
  167. absoluteRange?: AbsoluteTimeRange;
  168. }): ThunkResult<void> => {
  169. return dispatch => {
  170. dispatch(updateTime({ ...options }));
  171. dispatch(runQueries(options.exploreId));
  172. };
  173. };
  174. /**
  175. * Change the refresh interval of Explore. Called from the Refresh picker.
  176. */
  177. export function changeRefreshInterval(
  178. exploreId: ExploreId,
  179. refreshInterval: string
  180. ): ActionOf<ChangeRefreshIntervalPayload> {
  181. return changeRefreshIntervalAction({ exploreId, refreshInterval });
  182. }
  183. /**
  184. * Clear all queries and results.
  185. */
  186. export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
  187. return dispatch => {
  188. dispatch(scanStopAction({ exploreId }));
  189. dispatch(clearQueriesAction({ exploreId }));
  190. dispatch(stateSave());
  191. };
  192. }
  193. export function clearOrigin(): ThunkResult<void> {
  194. return dispatch => {
  195. dispatch(clearOriginAction({ exploreId: ExploreId.left }));
  196. };
  197. }
  198. /**
  199. * Loads all explore data sources and sets the chosen datasource.
  200. * If there are no datasources a missing datasource action is dispatched.
  201. */
  202. export function loadExploreDatasourcesAndSetDatasource(
  203. exploreId: ExploreId,
  204. datasourceName: string
  205. ): ThunkResult<void> {
  206. return dispatch => {
  207. const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
  208. .getExternal()
  209. .map(
  210. (ds: any) =>
  211. ({
  212. value: ds.name,
  213. name: ds.name,
  214. meta: ds.meta,
  215. } as DataSourceSelectItem)
  216. );
  217. dispatch(loadExploreDatasources({ exploreId, exploreDatasources }));
  218. if (exploreDatasources.length >= 1) {
  219. dispatch(changeDatasource(exploreId, datasourceName));
  220. } else {
  221. dispatch(loadDatasourceMissingAction({ exploreId }));
  222. }
  223. };
  224. }
  225. /**
  226. * Initialize Explore state with state from the URL and the React component.
  227. * Call this only on components for with the Explore state has not been initialized.
  228. */
  229. export function initializeExplore(
  230. exploreId: ExploreId,
  231. datasourceName: string,
  232. queries: DataQuery[],
  233. rawRange: RawTimeRange,
  234. mode: ExploreMode,
  235. containerWidth: number,
  236. eventBridge: Emitter,
  237. ui: ExploreUIState,
  238. originPanelId: number
  239. ): ThunkResult<void> {
  240. return async (dispatch, getState) => {
  241. const timeZone = getTimeZone(getState().user);
  242. const range = getTimeRange(timeZone, rawRange);
  243. dispatch(loadExploreDatasourcesAndSetDatasource(exploreId, datasourceName));
  244. dispatch(
  245. initializeExploreAction({
  246. exploreId,
  247. containerWidth,
  248. eventBridge,
  249. queries,
  250. range,
  251. mode,
  252. ui,
  253. originPanelId,
  254. })
  255. );
  256. dispatch(updateTime({ exploreId }));
  257. };
  258. }
  259. /**
  260. * Datasource loading was successfully completed.
  261. */
  262. export const loadDatasourceReady = (
  263. exploreId: ExploreId,
  264. instance: DataSourceApi,
  265. orgId: number
  266. ): ActionOf<LoadDatasourceReadyPayload> => {
  267. const historyKey = `grafana.explore.history.${instance.meta.id}`;
  268. const history = store.getObject(historyKey, []);
  269. // Save last-used datasource
  270. store.set(lastUsedDatasourceKeyForOrgId(orgId), instance.name);
  271. return loadDatasourceReadyAction({
  272. exploreId,
  273. history,
  274. });
  275. };
  276. export function importQueries(
  277. exploreId: ExploreId,
  278. queries: DataQuery[],
  279. sourceDataSource: DataSourceApi,
  280. targetDataSource: DataSourceApi
  281. ): ThunkResult<void> {
  282. return async dispatch => {
  283. if (!sourceDataSource) {
  284. // explore not initialized
  285. dispatch(queriesImportedAction({ exploreId, queries }));
  286. return;
  287. }
  288. let importedQueries = queries;
  289. // Check if queries can be imported from previously selected datasource
  290. if (sourceDataSource.meta.id === targetDataSource.meta.id) {
  291. // Keep same queries if same type of datasource
  292. importedQueries = [...queries];
  293. } else if (targetDataSource.importQueries) {
  294. // Datasource-specific importers
  295. importedQueries = await targetDataSource.importQueries(queries, sourceDataSource.meta);
  296. } else {
  297. // Default is blank queries
  298. importedQueries = ensureQueries();
  299. }
  300. const nextQueries = ensureQueries(importedQueries);
  301. dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
  302. };
  303. }
  304. /**
  305. * Tests datasource.
  306. */
  307. export const testDatasource = (exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> => {
  308. return async dispatch => {
  309. let datasourceError = null;
  310. dispatch(testDataSourcePendingAction({ exploreId }));
  311. try {
  312. const testResult = await instance.testDatasource();
  313. datasourceError = testResult.status === 'success' ? null : testResult.message;
  314. } catch (error) {
  315. datasourceError = (error && error.statusText) || 'Network error';
  316. }
  317. if (datasourceError) {
  318. dispatch(testDataSourceFailureAction({ exploreId, error: datasourceError }));
  319. return;
  320. }
  321. dispatch(testDataSourceSuccessAction({ exploreId }));
  322. };
  323. };
  324. /**
  325. * Reconnects datasource when there is a connection failure.
  326. */
  327. export const reconnectDatasource = (exploreId: ExploreId): ThunkResult<void> => {
  328. return async (dispatch, getState) => {
  329. const instance = getState().explore[exploreId].datasourceInstance;
  330. dispatch(changeDatasource(exploreId, instance.name));
  331. };
  332. };
  333. /**
  334. * Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
  335. */
  336. export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi, orgId: number): ThunkResult<void> {
  337. return async (dispatch, getState) => {
  338. const datasourceName = instance.name;
  339. // Keep ID to track selection
  340. dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
  341. await dispatch(testDatasource(exploreId, instance));
  342. if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
  343. // User already changed datasource again, discard results
  344. return;
  345. }
  346. if (instance.init) {
  347. try {
  348. instance.init();
  349. } catch (err) {
  350. console.log(err);
  351. }
  352. }
  353. if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
  354. // User already changed datasource again, discard results
  355. return;
  356. }
  357. dispatch(loadDatasourceReady(exploreId, instance, orgId));
  358. };
  359. }
  360. /**
  361. * Action to modify a query given a datasource-specific modifier action.
  362. * @param exploreId Explore area
  363. * @param modification Action object with a type, e.g., ADD_FILTER
  364. * @param index Optional query row index. If omitted, the modification is applied to all query rows.
  365. * @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
  366. */
  367. export function modifyQueries(
  368. exploreId: ExploreId,
  369. modification: QueryFixAction,
  370. index: number,
  371. modifier: any
  372. ): ThunkResult<void> {
  373. return dispatch => {
  374. dispatch(modifyQueriesAction({ exploreId, modification, index, modifier }));
  375. if (!modification.preventSubmit) {
  376. dispatch(runQueries(exploreId));
  377. }
  378. };
  379. }
  380. /**
  381. * Main action to run queries and dispatches sub-actions based on which result viewers are active
  382. */
  383. export function runQueries(exploreId: ExploreId): ThunkResult<void> {
  384. return (dispatch, getState) => {
  385. dispatch(updateTime({ exploreId }));
  386. const exploreItemState = getState().explore[exploreId];
  387. const {
  388. datasourceInstance,
  389. queries,
  390. datasourceError,
  391. containerWidth,
  392. isLive: live,
  393. queryState,
  394. queryIntervals,
  395. range,
  396. scanning,
  397. history,
  398. mode,
  399. } = exploreItemState;
  400. if (datasourceError) {
  401. // let's not run any queries if data source is in a faulty state
  402. return;
  403. }
  404. if (!hasNonEmptyQuery(queries)) {
  405. dispatch(clearQueriesAction({ exploreId }));
  406. dispatch(stateSave()); // Remember to save to state and update location
  407. return;
  408. }
  409. // Some datasource's query builders allow per-query interval limits,
  410. // but we're using the datasource interval limit for now
  411. const interval = datasourceInstance.interval;
  412. stopQueryState(queryState, 'New request issued');
  413. queryState.sendFrames = true;
  414. queryState.sendLegacy = true;
  415. const queryOptions = {
  416. interval,
  417. // This is used for logs streaming for buffer size.
  418. // TODO: not sure if this makes sense for normal query when using both graph and table
  419. maxDataPoints: mode === ExploreMode.Logs ? 1000 : containerWidth,
  420. live,
  421. };
  422. const datasourceId = datasourceInstance.meta.id;
  423. const transaction = buildQueryTransaction(queries, queryOptions, range, queryIntervals, scanning);
  424. queryState.onStreamingDataUpdated = () => {
  425. const response = queryState.validateStreamsAndGetPanelData();
  426. dispatch(queryStreamUpdatedAction({ exploreId, response }));
  427. };
  428. dispatch(queryStartAction({ exploreId }));
  429. queryState
  430. .execute(datasourceInstance, transaction.request)
  431. .then((response: PanelData) => {
  432. if (!response.error) {
  433. // Side-effect: Saving history in localstorage
  434. const nextHistory = updateHistory(history, datasourceId, queries);
  435. dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
  436. }
  437. dispatch(queryEndedAction({ exploreId, response }));
  438. dispatch(stateSave());
  439. // Keep scanning for results if this was the last scanning transaction
  440. if (getState().explore[exploreId].scanning) {
  441. if (_.size(response.series) === 0) {
  442. const range = getShiftedTimeRange(-1, getState().explore[exploreId].range);
  443. dispatch(updateTime({ exploreId, absoluteRange: range }));
  444. dispatch(runQueries(exploreId));
  445. } else {
  446. // We can stop scanning if we have a result
  447. dispatch(scanStopAction({ exploreId }));
  448. }
  449. }
  450. })
  451. .catch(error => {
  452. dispatch(
  453. queryEndedAction({
  454. exploreId,
  455. response: { error, legacy: [], series: [], request: transaction.request, state: LoadingState.Error },
  456. })
  457. );
  458. });
  459. };
  460. }
  461. const toRawTimeRange = (range: TimeRange): RawTimeRange => {
  462. let from = range.raw.from;
  463. if (isDateTime(from)) {
  464. from = from.valueOf().toString(10);
  465. }
  466. let to = range.raw.to;
  467. if (isDateTime(to)) {
  468. to = to.valueOf().toString(10);
  469. }
  470. return {
  471. from,
  472. to,
  473. };
  474. };
  475. export const stateSave = (): ThunkResult<void> => {
  476. return (dispatch, getState) => {
  477. const { left, right, split } = getState().explore;
  478. const orgId = getState().user.orgId.toString();
  479. const replace = left && left.urlReplaced === false;
  480. const urlStates: { [index: string]: string } = { orgId };
  481. const leftUrlState: ExploreUrlState = {
  482. datasource: left.datasourceInstance.name,
  483. queries: left.queries.map(clearQueryKeys),
  484. range: toRawTimeRange(left.range),
  485. mode: left.mode,
  486. ui: {
  487. showingGraph: left.showingGraph,
  488. showingLogs: true,
  489. showingTable: left.showingTable,
  490. dedupStrategy: left.dedupStrategy,
  491. },
  492. };
  493. urlStates.left = serializeStateToUrlParam(leftUrlState, true);
  494. if (split) {
  495. const rightUrlState: ExploreUrlState = {
  496. datasource: right.datasourceInstance.name,
  497. queries: right.queries.map(clearQueryKeys),
  498. range: toRawTimeRange(right.range),
  499. mode: right.mode,
  500. ui: {
  501. showingGraph: right.showingGraph,
  502. showingLogs: true,
  503. showingTable: right.showingTable,
  504. dedupStrategy: right.dedupStrategy,
  505. },
  506. };
  507. urlStates.right = serializeStateToUrlParam(rightUrlState, true);
  508. }
  509. dispatch(updateLocation({ query: urlStates, replace }));
  510. if (replace) {
  511. dispatch(setUrlReplacedAction({ exploreId: ExploreId.left }));
  512. }
  513. };
  514. };
  515. export const updateTime = (config: {
  516. exploreId: ExploreId;
  517. rawRange?: RawTimeRange;
  518. absoluteRange?: AbsoluteTimeRange;
  519. }): ThunkResult<void> => {
  520. return (dispatch, getState) => {
  521. const { exploreId, absoluteRange: absRange, rawRange: actionRange } = config;
  522. const itemState = getState().explore[exploreId];
  523. const timeZone = getTimeZone(getState().user);
  524. const { range: rangeInState } = itemState;
  525. let rawRange: RawTimeRange = rangeInState.raw;
  526. if (absRange) {
  527. rawRange = {
  528. from: dateTimeForTimeZone(timeZone, absRange.from),
  529. to: dateTimeForTimeZone(timeZone, absRange.to),
  530. };
  531. }
  532. if (actionRange) {
  533. rawRange = actionRange;
  534. }
  535. const range = getTimeRange(timeZone, rawRange);
  536. const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
  537. getTimeSrv().init({
  538. time: range.raw,
  539. refresh: false,
  540. getTimezone: () => timeZone,
  541. timeRangeUpdated: (): any => undefined,
  542. });
  543. dispatch(changeRangeAction({ exploreId, range, absoluteRange }));
  544. };
  545. };
  546. /**
  547. * Start a scan for more results using the given scanner.
  548. * @param exploreId Explore area
  549. * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
  550. */
  551. export function scanStart(exploreId: ExploreId): ThunkResult<void> {
  552. return (dispatch, getState) => {
  553. // Register the scanner
  554. dispatch(scanStartAction({ exploreId }));
  555. // Scanning must trigger query run, and return the new range
  556. const range = getShiftedTimeRange(-1, getState().explore[exploreId].range);
  557. // Set the new range to be displayed
  558. dispatch(updateTime({ exploreId, absoluteRange: range }));
  559. dispatch(runQueries(exploreId));
  560. };
  561. }
  562. /**
  563. * Reset queries to the given queries. Any modifications will be discarded.
  564. * Use this action for clicks on query examples. Triggers a query run.
  565. */
  566. export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
  567. return (dispatch, getState) => {
  568. // Inject react keys into query objects
  569. const queries = getState().explore[exploreId].queries;
  570. const nextQueries = rawQueries.map((query, index) => generateNewKeyAndAddRefIdIfMissing(query, queries, index));
  571. dispatch(setQueriesAction({ exploreId, queries: nextQueries }));
  572. dispatch(runQueries(exploreId));
  573. };
  574. }
  575. /**
  576. * Close the split view and save URL state.
  577. */
  578. export function splitClose(itemId: ExploreId): ThunkResult<void> {
  579. return dispatch => {
  580. dispatch(splitCloseAction({ itemId }));
  581. dispatch(stateSave());
  582. };
  583. }
  584. /**
  585. * Open the split view and copy the left state to be the right state.
  586. * The right state is automatically initialized.
  587. * The copy keeps all query modifications but wipes the query results.
  588. */
  589. export function splitOpen(): ThunkResult<void> {
  590. return (dispatch, getState) => {
  591. // Clone left state to become the right state
  592. const leftState = getState().explore[ExploreId.left];
  593. const queryState = getState().location.query[ExploreId.left] as string;
  594. const urlState = parseUrlState(queryState);
  595. const itemState: ExploreItemState = {
  596. ...leftState,
  597. queries: leftState.queries.slice(),
  598. urlState,
  599. };
  600. dispatch(splitOpenAction({ itemState }));
  601. dispatch(stateSave());
  602. };
  603. }
  604. /**
  605. * Creates action to collapse graph/logs/table panel. When panel is collapsed,
  606. * queries won't be run
  607. */
  608. const togglePanelActionCreator = (
  609. actionCreator: ActionCreator<ToggleGraphPayload> | ActionCreator<ToggleTablePayload>
  610. ) => (exploreId: ExploreId, isPanelVisible: boolean): ThunkResult<void> => {
  611. return dispatch => {
  612. let uiFragmentStateUpdate: Partial<ExploreUIState>;
  613. const shouldRunQueries = !isPanelVisible;
  614. switch (actionCreator.type) {
  615. case toggleGraphAction.type:
  616. uiFragmentStateUpdate = { showingGraph: !isPanelVisible };
  617. break;
  618. case toggleTableAction.type:
  619. uiFragmentStateUpdate = { showingTable: !isPanelVisible };
  620. break;
  621. }
  622. dispatch(actionCreator({ exploreId }));
  623. dispatch(updateExploreUIState(exploreId, uiFragmentStateUpdate));
  624. if (shouldRunQueries) {
  625. dispatch(runQueries(exploreId));
  626. }
  627. };
  628. };
  629. /**
  630. * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
  631. */
  632. export const toggleGraph = togglePanelActionCreator(toggleGraphAction);
  633. /**
  634. * Expand/collapse the table result viewer. When collapsed, table queries won't be run.
  635. */
  636. export const toggleTable = togglePanelActionCreator(toggleTableAction);
  637. /**
  638. * Change logs deduplication strategy and update URL.
  639. */
  640. export const changeDedupStrategy = (exploreId: ExploreId, dedupStrategy: LogsDedupStrategy): ThunkResult<void> => {
  641. return dispatch => {
  642. dispatch(updateExploreUIState(exploreId, { dedupStrategy }));
  643. };
  644. };
  645. export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
  646. return (dispatch, getState) => {
  647. const itemState = getState().explore[exploreId];
  648. if (!itemState.initialized) {
  649. return;
  650. }
  651. const { urlState, update, containerWidth, eventBridge } = itemState;
  652. const { datasource, queries, range: urlRange, mode, ui, originPanelId } = urlState;
  653. const refreshQueries: DataQuery[] = [];
  654. for (let index = 0; index < queries.length; index++) {
  655. const query = queries[index];
  656. refreshQueries.push(generateNewKeyAndAddRefIdIfMissing(query, refreshQueries, index));
  657. }
  658. const timeZone = getTimeZone(getState().user);
  659. const range = getTimeRangeFromUrl(urlRange, timeZone);
  660. // need to refresh datasource
  661. if (update.datasource) {
  662. const initialQueries = ensureQueries(queries);
  663. dispatch(
  664. initializeExplore(
  665. exploreId,
  666. datasource,
  667. initialQueries,
  668. range,
  669. mode,
  670. containerWidth,
  671. eventBridge,
  672. ui,
  673. originPanelId
  674. )
  675. );
  676. return;
  677. }
  678. if (update.range) {
  679. dispatch(updateTime({ exploreId, rawRange: range.raw }));
  680. }
  681. // need to refresh ui state
  682. if (update.ui) {
  683. dispatch(updateUIStateAction({ ...ui, exploreId }));
  684. }
  685. // need to refresh queries
  686. if (update.queries) {
  687. dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
  688. }
  689. // need to refresh mode
  690. if (update.mode) {
  691. dispatch(changeModeAction({ exploreId, mode }));
  692. }
  693. // always run queries when refresh is needed
  694. if (update.queries || update.ui || update.range) {
  695. dispatch(runQueries(exploreId));
  696. }
  697. };
  698. }