reducers.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. import _ from 'lodash';
  2. import {
  3. getIntervals,
  4. ensureQueries,
  5. getQueryKeys,
  6. parseUrlState,
  7. DEFAULT_UI_STATE,
  8. generateNewKeyAndAddRefIdIfMissing,
  9. sortLogsResult,
  10. } from 'app/core/utils/explore';
  11. import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState, ExploreMode } from 'app/types/explore';
  12. import { LoadingState } from '@grafana/data';
  13. import { DataQuery } from '@grafana/ui';
  14. import {
  15. HigherOrderAction,
  16. ActionTypes,
  17. testDataSourcePendingAction,
  18. testDataSourceSuccessAction,
  19. testDataSourceFailureAction,
  20. splitCloseAction,
  21. SplitCloseActionPayload,
  22. loadExploreDatasources,
  23. historyUpdatedAction,
  24. changeModeAction,
  25. queryFailureAction,
  26. setUrlReplacedAction,
  27. querySuccessAction,
  28. scanStopAction,
  29. resetQueryErrorAction,
  30. queryStartAction,
  31. runQueriesAction,
  32. changeRangeAction,
  33. } from './actionTypes';
  34. import { reducerFactory } from 'app/core/redux';
  35. import {
  36. addQueryRowAction,
  37. changeQueryAction,
  38. changeSizeAction,
  39. changeRefreshIntervalAction,
  40. clearQueriesAction,
  41. highlightLogsExpressionAction,
  42. initializeExploreAction,
  43. updateDatasourceInstanceAction,
  44. loadDatasourceMissingAction,
  45. loadDatasourcePendingAction,
  46. loadDatasourceReadyAction,
  47. modifyQueriesAction,
  48. removeQueryRowAction,
  49. scanStartAction,
  50. setQueriesAction,
  51. toggleTableAction,
  52. queriesImportedAction,
  53. updateUIStateAction,
  54. toggleLogLevelAction,
  55. } from './actionTypes';
  56. import { updateLocation } from 'app/core/actions/location';
  57. import { LocationUpdate } from '@grafana/runtime';
  58. import TableModel from 'app/core/table_model';
  59. import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
  60. export const DEFAULT_RANGE = {
  61. from: 'now-6h',
  62. to: 'now',
  63. };
  64. // Millies step for helper bar charts
  65. const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
  66. export const makeInitialUpdateState = (): ExploreUpdateState => ({
  67. datasource: false,
  68. queries: false,
  69. range: false,
  70. mode: false,
  71. ui: false,
  72. });
  73. /**
  74. * Returns a fresh Explore area state
  75. */
  76. export const makeExploreItemState = (): ExploreItemState => ({
  77. StartPage: undefined,
  78. containerWidth: 0,
  79. datasourceInstance: null,
  80. requestedDatasourceName: null,
  81. datasourceError: null,
  82. datasourceLoading: null,
  83. datasourceMissing: false,
  84. exploreDatasources: [],
  85. history: [],
  86. queries: [],
  87. initialized: false,
  88. queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
  89. range: {
  90. from: null,
  91. to: null,
  92. raw: DEFAULT_RANGE,
  93. },
  94. absoluteRange: {
  95. from: null,
  96. to: null,
  97. },
  98. scanning: false,
  99. scanRange: null,
  100. showingGraph: true,
  101. showingTable: true,
  102. loadingState: LoadingState.NotStarted,
  103. queryKeys: [],
  104. urlState: null,
  105. update: makeInitialUpdateState(),
  106. queryErrors: [],
  107. latency: 0,
  108. supportedModes: [],
  109. mode: null,
  110. isLive: false,
  111. urlReplaced: false,
  112. });
  113. /**
  114. * Global Explore state that handles multiple Explore areas and the split state
  115. */
  116. export const initialExploreItemState = makeExploreItemState();
  117. export const initialExploreState: ExploreState = {
  118. split: null,
  119. left: initialExploreItemState,
  120. right: initialExploreItemState,
  121. };
  122. /**
  123. * Reducer for an Explore area, to be used by the global Explore reducer.
  124. */
  125. export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemState)
  126. .addMapper({
  127. filter: addQueryRowAction,
  128. mapper: (state, action): ExploreItemState => {
  129. const { queries } = state;
  130. const { index, query } = action.payload;
  131. // Add to queries, which will cause a new row to be rendered
  132. const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)];
  133. return {
  134. ...state,
  135. queries: nextQueries,
  136. logsHighlighterExpressions: undefined,
  137. queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
  138. };
  139. },
  140. })
  141. .addMapper({
  142. filter: changeQueryAction,
  143. mapper: (state, action): ExploreItemState => {
  144. const { queries } = state;
  145. const { query, index } = action.payload;
  146. // Override path: queries are completely reset
  147. const nextQuery: DataQuery = generateNewKeyAndAddRefIdIfMissing(query, queries, index);
  148. const nextQueries = [...queries];
  149. nextQueries[index] = nextQuery;
  150. return {
  151. ...state,
  152. queries: nextQueries,
  153. queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
  154. };
  155. },
  156. })
  157. .addMapper({
  158. filter: changeSizeAction,
  159. mapper: (state, action): ExploreItemState => {
  160. const containerWidth = action.payload.width;
  161. return { ...state, containerWidth };
  162. },
  163. })
  164. .addMapper({
  165. filter: changeModeAction,
  166. mapper: (state, action): ExploreItemState => {
  167. const mode = action.payload.mode;
  168. return { ...state, mode };
  169. },
  170. })
  171. .addMapper({
  172. filter: changeRefreshIntervalAction,
  173. mapper: (state, action): ExploreItemState => {
  174. const { refreshInterval } = action.payload;
  175. const live = isLive(refreshInterval);
  176. const logsResult = sortLogsResult(state.logsResult, refreshInterval);
  177. return {
  178. ...state,
  179. refreshInterval,
  180. loadingState: live ? LoadingState.Streaming : LoadingState.NotStarted,
  181. isLive: live,
  182. logsResult,
  183. };
  184. },
  185. })
  186. .addMapper({
  187. filter: clearQueriesAction,
  188. mapper: (state): ExploreItemState => {
  189. const queries = ensureQueries();
  190. return {
  191. ...state,
  192. queries: queries.slice(),
  193. showingStartPage: Boolean(state.StartPage),
  194. queryKeys: getQueryKeys(queries, state.datasourceInstance),
  195. };
  196. },
  197. })
  198. .addMapper({
  199. filter: highlightLogsExpressionAction,
  200. mapper: (state, action): ExploreItemState => {
  201. const { expressions } = action.payload;
  202. return { ...state, logsHighlighterExpressions: expressions };
  203. },
  204. })
  205. .addMapper({
  206. filter: initializeExploreAction,
  207. mapper: (state, action): ExploreItemState => {
  208. const { containerWidth, eventBridge, queries, range, mode, ui } = action.payload;
  209. return {
  210. ...state,
  211. containerWidth,
  212. eventBridge,
  213. range,
  214. mode,
  215. queries,
  216. initialized: true,
  217. queryKeys: getQueryKeys(queries, state.datasourceInstance),
  218. ...ui,
  219. update: makeInitialUpdateState(),
  220. };
  221. },
  222. })
  223. .addMapper({
  224. filter: updateDatasourceInstanceAction,
  225. mapper: (state, action): ExploreItemState => {
  226. const { datasourceInstance } = action.payload;
  227. // Capabilities
  228. const supportsGraph = datasourceInstance.meta.metrics;
  229. const supportsLogs = datasourceInstance.meta.logs;
  230. let mode = state.mode || ExploreMode.Metrics;
  231. const supportedModes: ExploreMode[] = [];
  232. if (supportsGraph) {
  233. supportedModes.push(ExploreMode.Metrics);
  234. }
  235. if (supportsLogs) {
  236. supportedModes.push(ExploreMode.Logs);
  237. }
  238. if (supportedModes.length === 1) {
  239. mode = supportedModes[0];
  240. }
  241. // Custom components
  242. const StartPage = datasourceInstance.components.ExploreStartPage;
  243. return {
  244. ...state,
  245. datasourceInstance,
  246. queryErrors: [],
  247. latency: 0,
  248. loadingState: LoadingState.NotStarted,
  249. StartPage,
  250. showingStartPage: Boolean(StartPage),
  251. queryKeys: [],
  252. supportedModes,
  253. mode,
  254. };
  255. },
  256. })
  257. .addMapper({
  258. filter: loadDatasourceMissingAction,
  259. mapper: (state): ExploreItemState => {
  260. return {
  261. ...state,
  262. datasourceMissing: true,
  263. datasourceLoading: false,
  264. update: makeInitialUpdateState(),
  265. };
  266. },
  267. })
  268. .addMapper({
  269. filter: loadDatasourcePendingAction,
  270. mapper: (state, action): ExploreItemState => {
  271. return {
  272. ...state,
  273. datasourceLoading: true,
  274. requestedDatasourceName: action.payload.requestedDatasourceName,
  275. };
  276. },
  277. })
  278. .addMapper({
  279. filter: loadDatasourceReadyAction,
  280. mapper: (state, action): ExploreItemState => {
  281. const { history } = action.payload;
  282. return {
  283. ...state,
  284. history,
  285. datasourceLoading: false,
  286. datasourceMissing: false,
  287. logsHighlighterExpressions: undefined,
  288. update: makeInitialUpdateState(),
  289. };
  290. },
  291. })
  292. .addMapper({
  293. filter: modifyQueriesAction,
  294. mapper: (state, action): ExploreItemState => {
  295. const { queries } = state;
  296. const { modification, index, modifier } = action.payload;
  297. let nextQueries: DataQuery[];
  298. if (index === undefined) {
  299. // Modify all queries
  300. nextQueries = queries.map((query, i) => {
  301. const nextQuery = modifier({ ...query }, modification);
  302. return generateNewKeyAndAddRefIdIfMissing(nextQuery, queries, i);
  303. });
  304. } else {
  305. // Modify query only at index
  306. nextQueries = queries.map((query, i) => {
  307. if (i === index) {
  308. const nextQuery = modifier({ ...query }, modification);
  309. return generateNewKeyAndAddRefIdIfMissing(nextQuery, queries, i);
  310. }
  311. return query;
  312. });
  313. }
  314. return {
  315. ...state,
  316. queries: nextQueries,
  317. queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
  318. };
  319. },
  320. })
  321. .addMapper({
  322. filter: queryFailureAction,
  323. mapper: (state, action): ExploreItemState => {
  324. const { response } = action.payload;
  325. const queryErrors = state.queryErrors.concat(response);
  326. return {
  327. ...state,
  328. graphResult: null,
  329. tableResult: null,
  330. logsResult: null,
  331. latency: 0,
  332. queryErrors,
  333. loadingState: LoadingState.Error,
  334. update: makeInitialUpdateState(),
  335. };
  336. },
  337. })
  338. .addMapper({
  339. filter: queryStartAction,
  340. mapper: (state): ExploreItemState => {
  341. return {
  342. ...state,
  343. queryErrors: [],
  344. latency: 0,
  345. loadingState: LoadingState.Loading,
  346. update: makeInitialUpdateState(),
  347. };
  348. },
  349. })
  350. .addMapper({
  351. filter: querySuccessAction,
  352. mapper: (state, action): ExploreItemState => {
  353. const { latency, loadingState, graphResult, tableResult, logsResult } = action.payload;
  354. return {
  355. ...state,
  356. loadingState,
  357. graphResult,
  358. tableResult,
  359. logsResult,
  360. latency,
  361. showingStartPage: false,
  362. update: makeInitialUpdateState(),
  363. };
  364. },
  365. })
  366. .addMapper({
  367. filter: removeQueryRowAction,
  368. mapper: (state, action): ExploreItemState => {
  369. const { queries, queryKeys } = state;
  370. const { index } = action.payload;
  371. if (queries.length <= 1) {
  372. return state;
  373. }
  374. const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
  375. const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)];
  376. return {
  377. ...state,
  378. queries: nextQueries,
  379. logsHighlighterExpressions: undefined,
  380. queryKeys: nextQueryKeys,
  381. };
  382. },
  383. })
  384. .addMapper({
  385. filter: scanStartAction,
  386. mapper: (state, action): ExploreItemState => {
  387. return { ...state, scanning: true };
  388. },
  389. })
  390. .addMapper({
  391. filter: scanStopAction,
  392. mapper: (state): ExploreItemState => {
  393. return {
  394. ...state,
  395. scanning: false,
  396. scanRange: undefined,
  397. update: makeInitialUpdateState(),
  398. };
  399. },
  400. })
  401. .addMapper({
  402. filter: setQueriesAction,
  403. mapper: (state, action): ExploreItemState => {
  404. const { queries } = action.payload;
  405. return {
  406. ...state,
  407. queries: queries.slice(),
  408. queryKeys: getQueryKeys(queries, state.datasourceInstance),
  409. };
  410. },
  411. })
  412. .addMapper({
  413. filter: updateUIStateAction,
  414. mapper: (state, action): ExploreItemState => {
  415. return { ...state, ...action.payload };
  416. },
  417. })
  418. .addMapper({
  419. filter: toggleTableAction,
  420. mapper: (state): ExploreItemState => {
  421. const showingTable = !state.showingTable;
  422. if (showingTable) {
  423. return { ...state };
  424. }
  425. return { ...state, tableResult: new TableModel() };
  426. },
  427. })
  428. .addMapper({
  429. filter: queriesImportedAction,
  430. mapper: (state, action): ExploreItemState => {
  431. const { queries } = action.payload;
  432. return {
  433. ...state,
  434. queries,
  435. queryKeys: getQueryKeys(queries, state.datasourceInstance),
  436. };
  437. },
  438. })
  439. .addMapper({
  440. filter: toggleLogLevelAction,
  441. mapper: (state, action): ExploreItemState => {
  442. const { hiddenLogLevels } = action.payload;
  443. return {
  444. ...state,
  445. hiddenLogLevels: Array.from(hiddenLogLevels),
  446. };
  447. },
  448. })
  449. .addMapper({
  450. filter: testDataSourcePendingAction,
  451. mapper: (state): ExploreItemState => {
  452. return {
  453. ...state,
  454. datasourceError: null,
  455. };
  456. },
  457. })
  458. .addMapper({
  459. filter: testDataSourceSuccessAction,
  460. mapper: (state): ExploreItemState => {
  461. return {
  462. ...state,
  463. datasourceError: null,
  464. };
  465. },
  466. })
  467. .addMapper({
  468. filter: testDataSourceFailureAction,
  469. mapper: (state, action): ExploreItemState => {
  470. return {
  471. ...state,
  472. datasourceError: action.payload.error,
  473. graphResult: undefined,
  474. tableResult: undefined,
  475. logsResult: undefined,
  476. update: makeInitialUpdateState(),
  477. };
  478. },
  479. })
  480. .addMapper({
  481. filter: loadExploreDatasources,
  482. mapper: (state, action): ExploreItemState => {
  483. return {
  484. ...state,
  485. exploreDatasources: action.payload.exploreDatasources,
  486. };
  487. },
  488. })
  489. .addMapper({
  490. filter: runQueriesAction,
  491. mapper: (state): ExploreItemState => {
  492. const { range } = state;
  493. const { datasourceInstance, containerWidth } = state;
  494. let interval = '1s';
  495. if (datasourceInstance && datasourceInstance.interval) {
  496. interval = datasourceInstance.interval;
  497. }
  498. const queryIntervals = getIntervals(range, interval, containerWidth);
  499. return {
  500. ...state,
  501. range,
  502. queryIntervals,
  503. showingStartPage: false,
  504. };
  505. },
  506. })
  507. .addMapper({
  508. filter: historyUpdatedAction,
  509. mapper: (state, action): ExploreItemState => {
  510. return {
  511. ...state,
  512. history: action.payload.history,
  513. };
  514. },
  515. })
  516. .addMapper({
  517. filter: resetQueryErrorAction,
  518. mapper: (state, action): ExploreItemState => {
  519. const { refIds } = action.payload;
  520. const queryErrors = state.queryErrors.reduce((allErrors, error) => {
  521. if (error.refId && refIds.indexOf(error.refId) !== -1) {
  522. return allErrors;
  523. }
  524. return allErrors.concat(error);
  525. }, []);
  526. return {
  527. ...state,
  528. queryErrors,
  529. };
  530. },
  531. })
  532. .addMapper({
  533. filter: setUrlReplacedAction,
  534. mapper: (state): ExploreItemState => {
  535. return {
  536. ...state,
  537. urlReplaced: true,
  538. };
  539. },
  540. })
  541. .addMapper({
  542. filter: changeRangeAction,
  543. mapper: (state, action): ExploreItemState => {
  544. const { range, absoluteRange } = action.payload;
  545. return {
  546. ...state,
  547. range,
  548. absoluteRange,
  549. };
  550. },
  551. })
  552. .create();
  553. export const updateChildRefreshState = (
  554. state: Readonly<ExploreItemState>,
  555. payload: LocationUpdate,
  556. exploreId: ExploreId
  557. ): ExploreItemState => {
  558. const path = payload.path || '';
  559. const queryState = payload.query[exploreId] as string;
  560. if (!queryState) {
  561. return state;
  562. }
  563. const urlState = parseUrlState(queryState);
  564. if (!state.urlState || path !== '/explore') {
  565. // we only want to refresh when browser back/forward
  566. return {
  567. ...state,
  568. urlState,
  569. update: { datasource: false, queries: false, range: false, mode: false, ui: false },
  570. };
  571. }
  572. const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;
  573. const queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === false;
  574. const range = _.isEqual(urlState ? urlState.range : DEFAULT_RANGE, state.urlState.range) === false;
  575. const mode = _.isEqual(urlState ? urlState.mode : ExploreMode.Metrics, state.urlState.mode) === false;
  576. const ui = _.isEqual(urlState ? urlState.ui : DEFAULT_UI_STATE, state.urlState.ui) === false;
  577. return {
  578. ...state,
  579. urlState,
  580. update: {
  581. ...state.update,
  582. datasource,
  583. queries,
  584. range,
  585. mode,
  586. ui,
  587. },
  588. };
  589. };
  590. /**
  591. * Global Explore reducer that handles multiple Explore areas (left and right).
  592. * Actions that have an `exploreId` get routed to the ExploreItemReducer.
  593. */
  594. export const exploreReducer = (state = initialExploreState, action: HigherOrderAction): ExploreState => {
  595. switch (action.type) {
  596. case splitCloseAction.type: {
  597. const { itemId } = action.payload as SplitCloseActionPayload;
  598. const targetSplit = {
  599. left: itemId === ExploreId.left ? state.right : state.left,
  600. right: initialExploreState.right,
  601. };
  602. return {
  603. ...state,
  604. ...targetSplit,
  605. split: false,
  606. };
  607. }
  608. case ActionTypes.SplitOpen: {
  609. return { ...state, split: true, right: { ...action.payload.itemState } };
  610. }
  611. case ActionTypes.ResetExplore: {
  612. return initialExploreState;
  613. }
  614. case updateLocation.type: {
  615. const { query } = action.payload;
  616. if (!query || !query[ExploreId.left]) {
  617. return state;
  618. }
  619. const split = query[ExploreId.right] ? true : false;
  620. const leftState = state[ExploreId.left];
  621. const rightState = state[ExploreId.right];
  622. return {
  623. ...state,
  624. split,
  625. [ExploreId.left]: updateChildRefreshState(leftState, action.payload, ExploreId.left),
  626. [ExploreId.right]: updateChildRefreshState(rightState, action.payload, ExploreId.right),
  627. };
  628. }
  629. }
  630. if (action.payload) {
  631. const { exploreId } = action.payload as any;
  632. if (exploreId !== undefined) {
  633. const exploreItemState = state[exploreId];
  634. return { ...state, [exploreId]: itemReducer(exploreItemState, action) };
  635. }
  636. }
  637. return state;
  638. };
  639. export default {
  640. explore: exploreReducer,
  641. };