reducers.ts 18 KB

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