reducers.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  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 { HigherOrderAction, ActionTypes } from './actionTypes';
  15. import { reducerFactory } from 'app/core/redux';
  16. import {
  17. addQueryRowAction,
  18. changeQueryAction,
  19. changeSizeAction,
  20. changeTimeAction,
  21. clearQueriesAction,
  22. highlightLogsExpressionAction,
  23. initializeExploreAction,
  24. updateDatasourceInstanceAction,
  25. loadDatasourceFailureAction,
  26. loadDatasourceMissingAction,
  27. loadDatasourcePendingAction,
  28. loadDatasourceSuccessAction,
  29. modifyQueriesAction,
  30. queryTransactionFailureAction,
  31. queryTransactionStartAction,
  32. queryTransactionSuccessAction,
  33. removeQueryRowAction,
  34. scanRangeAction,
  35. scanStartAction,
  36. scanStopAction,
  37. setQueriesAction,
  38. toggleGraphAction,
  39. toggleLogsAction,
  40. toggleTableAction,
  41. queriesImportedAction,
  42. updateUIStateAction,
  43. toggleLogLevelAction,
  44. } from './actionTypes';
  45. import { updateLocation } from 'app/core/actions/location';
  46. import { LocationUpdate } from 'app/types';
  47. export const DEFAULT_RANGE = {
  48. from: 'now-6h',
  49. to: 'now',
  50. };
  51. // Millies step for helper bar charts
  52. const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
  53. export const makeInitialUpdateState = (): ExploreUpdateState => ({
  54. datasource: false,
  55. queries: false,
  56. range: false,
  57. ui: false,
  58. });
  59. /**
  60. * Returns a fresh Explore area state
  61. */
  62. export const makeExploreItemState = (): ExploreItemState => ({
  63. StartPage: undefined,
  64. containerWidth: 0,
  65. datasourceInstance: null,
  66. requestedDatasourceName: null,
  67. datasourceError: null,
  68. datasourceLoading: null,
  69. datasourceMissing: false,
  70. exploreDatasources: [],
  71. history: [],
  72. queries: [],
  73. initialized: false,
  74. queryTransactions: [],
  75. queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
  76. range: DEFAULT_RANGE,
  77. scanning: false,
  78. scanRange: null,
  79. showingGraph: true,
  80. showingLogs: true,
  81. showingTable: true,
  82. supportsGraph: null,
  83. supportsLogs: null,
  84. supportsTable: null,
  85. queryKeys: [],
  86. urlState: null,
  87. update: makeInitialUpdateState(),
  88. });
  89. /**
  90. * Global Explore state that handles multiple Explore areas and the split state
  91. */
  92. export const initialExploreState: ExploreState = {
  93. split: null,
  94. left: makeExploreItemState(),
  95. right: makeExploreItemState(),
  96. };
  97. /**
  98. * Reducer for an Explore area, to be used by the global Explore reducer.
  99. */
  100. export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemState)
  101. .addMapper({
  102. filter: addQueryRowAction,
  103. mapper: (state, action): ExploreItemState => {
  104. const { queries, queryTransactions } = state;
  105. const { index, query } = action.payload;
  106. // Add to queries, which will cause a new row to be rendered
  107. const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)];
  108. // Ongoing transactions need to update their row indices
  109. const nextQueryTransactions = queryTransactions.map(qt => {
  110. if (qt.rowIndex > index) {
  111. return {
  112. ...qt,
  113. rowIndex: qt.rowIndex + 1,
  114. };
  115. }
  116. return qt;
  117. });
  118. return {
  119. ...state,
  120. queries: nextQueries,
  121. logsHighlighterExpressions: undefined,
  122. queryTransactions: nextQueryTransactions,
  123. queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
  124. };
  125. },
  126. })
  127. .addMapper({
  128. filter: changeQueryAction,
  129. mapper: (state, action): ExploreItemState => {
  130. const { queries, queryTransactions } = state;
  131. const { query, index } = action.payload;
  132. // Override path: queries are completely reset
  133. const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(state.queries) };
  134. const nextQueries = [...queries];
  135. nextQueries[index] = nextQuery;
  136. // Discard ongoing transaction related to row query
  137. const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
  138. return {
  139. ...state,
  140. queries: nextQueries,
  141. queryTransactions: nextQueryTransactions,
  142. queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
  143. };
  144. },
  145. })
  146. .addMapper({
  147. filter: changeSizeAction,
  148. mapper: (state, action): ExploreItemState => {
  149. const { range, datasourceInstance } = state;
  150. let interval = '1s';
  151. if (datasourceInstance && datasourceInstance.interval) {
  152. interval = datasourceInstance.interval;
  153. }
  154. const containerWidth = action.payload.width;
  155. const queryIntervals = getIntervals(range, interval, containerWidth);
  156. return { ...state, containerWidth, queryIntervals };
  157. },
  158. })
  159. .addMapper({
  160. filter: changeTimeAction,
  161. mapper: (state, action): ExploreItemState => {
  162. return { ...state, range: action.payload.range };
  163. },
  164. })
  165. .addMapper({
  166. filter: clearQueriesAction,
  167. mapper: (state): ExploreItemState => {
  168. const queries = ensureQueries();
  169. return {
  170. ...state,
  171. queries: queries.slice(),
  172. queryTransactions: [],
  173. showingStartPage: Boolean(state.StartPage),
  174. queryKeys: getQueryKeys(queries, state.datasourceInstance),
  175. };
  176. },
  177. })
  178. .addMapper({
  179. filter: highlightLogsExpressionAction,
  180. mapper: (state, action): ExploreItemState => {
  181. const { expressions } = action.payload;
  182. return { ...state, logsHighlighterExpressions: expressions };
  183. },
  184. })
  185. .addMapper({
  186. filter: initializeExploreAction,
  187. mapper: (state, action): ExploreItemState => {
  188. const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
  189. return {
  190. ...state,
  191. containerWidth,
  192. eventBridge,
  193. exploreDatasources,
  194. range,
  195. queries,
  196. initialized: true,
  197. queryKeys: getQueryKeys(queries, state.datasourceInstance),
  198. ...ui,
  199. update: makeInitialUpdateState(),
  200. };
  201. },
  202. })
  203. .addMapper({
  204. filter: updateDatasourceInstanceAction,
  205. mapper: (state, action): ExploreItemState => {
  206. const { datasourceInstance } = action.payload;
  207. return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.queries, datasourceInstance) };
  208. },
  209. })
  210. .addMapper({
  211. filter: loadDatasourceFailureAction,
  212. mapper: (state, action): ExploreItemState => {
  213. return {
  214. ...state,
  215. datasourceError: action.payload.error,
  216. datasourceLoading: false,
  217. update: makeInitialUpdateState(),
  218. };
  219. },
  220. })
  221. .addMapper({
  222. filter: loadDatasourceMissingAction,
  223. mapper: (state): ExploreItemState => {
  224. return {
  225. ...state,
  226. datasourceMissing: true,
  227. datasourceLoading: false,
  228. update: makeInitialUpdateState(),
  229. };
  230. },
  231. })
  232. .addMapper({
  233. filter: loadDatasourcePendingAction,
  234. mapper: (state, action): ExploreItemState => {
  235. return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName };
  236. },
  237. })
  238. .addMapper({
  239. filter: loadDatasourceSuccessAction,
  240. mapper: (state, action): ExploreItemState => {
  241. const { containerWidth, range } = state;
  242. const {
  243. StartPage,
  244. datasourceInstance,
  245. history,
  246. showingStartPage,
  247. supportsGraph,
  248. supportsLogs,
  249. supportsTable,
  250. } = action.payload;
  251. const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth);
  252. return {
  253. ...state,
  254. queryIntervals,
  255. StartPage,
  256. datasourceInstance,
  257. history,
  258. showingStartPage,
  259. supportsGraph,
  260. supportsLogs,
  261. supportsTable,
  262. datasourceLoading: false,
  263. datasourceMissing: false,
  264. datasourceError: null,
  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. .create();
  499. export const updateChildRefreshState = (
  500. state: Readonly<ExploreItemState>,
  501. payload: LocationUpdate,
  502. exploreId: ExploreId
  503. ): ExploreItemState => {
  504. const path = payload.path || '';
  505. const queryState = payload.query[exploreId] as string;
  506. if (!queryState) {
  507. return state;
  508. }
  509. const urlState = parseUrlState(queryState);
  510. if (!state.urlState || path !== '/explore') {
  511. // we only want to refresh when browser back/forward
  512. return { ...state, urlState, update: { datasource: false, queries: false, range: false, ui: false } };
  513. }
  514. const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;
  515. const queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === false;
  516. const range = _.isEqual(urlState ? urlState.range : DEFAULT_RANGE, state.urlState.range) === false;
  517. const ui = _.isEqual(urlState ? urlState.ui : DEFAULT_UI_STATE, state.urlState.ui) === false;
  518. return {
  519. ...state,
  520. urlState,
  521. update: {
  522. ...state.update,
  523. datasource,
  524. queries,
  525. range,
  526. ui,
  527. },
  528. };
  529. };
  530. /**
  531. * Global Explore reducer that handles multiple Explore areas (left and right).
  532. * Actions that have an `exploreId` get routed to the ExploreItemReducer.
  533. */
  534. export const exploreReducer = (state = initialExploreState, action: HigherOrderAction): ExploreState => {
  535. switch (action.type) {
  536. case ActionTypes.SplitClose: {
  537. return { ...state, split: false };
  538. }
  539. case ActionTypes.SplitOpen: {
  540. return { ...state, split: true, right: { ...action.payload.itemState } };
  541. }
  542. case ActionTypes.ResetExplore: {
  543. return initialExploreState;
  544. }
  545. case updateLocation.type: {
  546. const { query } = action.payload;
  547. if (!query || !query[ExploreId.left]) {
  548. return state;
  549. }
  550. const split = query[ExploreId.right] ? true : false;
  551. const leftState = state[ExploreId.left];
  552. const rightState = state[ExploreId.right];
  553. return {
  554. ...state,
  555. split,
  556. [ExploreId.left]: updateChildRefreshState(leftState, action.payload, ExploreId.left),
  557. [ExploreId.right]: updateChildRefreshState(rightState, action.payload, ExploreId.right),
  558. };
  559. }
  560. }
  561. if (action.payload) {
  562. const { exploreId } = action.payload as any;
  563. if (exploreId !== undefined) {
  564. const exploreItemState = state[exploreId];
  565. return { ...state, [exploreId]: itemReducer(exploreItemState, action) };
  566. }
  567. }
  568. return state;
  569. };
  570. export default {
  571. explore: exploreReducer,
  572. };