reducers.ts 19 KB

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