actions.ts 24 KB

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