actions.ts 27 KB

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