reducers.ts 18 KB

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