reducers.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. import {
  2. calculateResultsFromQueryTransactions,
  3. generateEmptyQuery,
  4. getIntervals,
  5. ensureQueries,
  6. getQueryKeys,
  7. } from 'app/core/utils/explore';
  8. import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
  9. import { DataQuery } from '@grafana/ui/src/types';
  10. import { HigherOrderAction, ActionTypes } from './actionTypes';
  11. import { reducerFactory } from 'app/core/redux';
  12. import {
  13. addQueryRowAction,
  14. changeQueryAction,
  15. changeSizeAction,
  16. changeTimeAction,
  17. clearQueriesAction,
  18. highlightLogsExpressionAction,
  19. initializeExploreAction,
  20. updateDatasourceInstanceAction,
  21. loadDatasourceFailureAction,
  22. loadDatasourceMissingAction,
  23. loadDatasourcePendingAction,
  24. loadDatasourceSuccessAction,
  25. modifyQueriesAction,
  26. queryTransactionFailureAction,
  27. queryTransactionStartAction,
  28. queryTransactionSuccessAction,
  29. removeQueryRowAction,
  30. runQueriesEmptyAction,
  31. scanRangeAction,
  32. scanStartAction,
  33. scanStopAction,
  34. setQueriesAction,
  35. toggleGraphAction,
  36. toggleLogsAction,
  37. toggleTableAction,
  38. queriesImportedAction,
  39. updateUIStateAction,
  40. } from './actionTypes';
  41. export const DEFAULT_RANGE = {
  42. from: 'now-6h',
  43. to: 'now',
  44. };
  45. // Millies step for helper bar charts
  46. const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
  47. /**
  48. * Returns a fresh Explore area state
  49. */
  50. export const makeExploreItemState = (): ExploreItemState => ({
  51. StartPage: undefined,
  52. containerWidth: 0,
  53. datasourceInstance: null,
  54. requestedDatasourceName: null,
  55. datasourceError: null,
  56. datasourceLoading: null,
  57. datasourceMissing: false,
  58. exploreDatasources: [],
  59. history: [],
  60. queries: [],
  61. initialized: false,
  62. queryTransactions: [],
  63. queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
  64. range: DEFAULT_RANGE,
  65. scanning: false,
  66. scanRange: null,
  67. showingGraph: true,
  68. showingLogs: true,
  69. showingTable: true,
  70. supportsGraph: null,
  71. supportsLogs: null,
  72. supportsTable: null,
  73. queryKeys: [],
  74. });
  75. /**
  76. * Global Explore state that handles multiple Explore areas and the split state
  77. */
  78. export const initialExploreState: ExploreState = {
  79. split: null,
  80. left: makeExploreItemState(),
  81. right: makeExploreItemState(),
  82. };
  83. /**
  84. * Reducer for an Explore area, to be used by the global Explore reducer.
  85. */
  86. export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemState)
  87. .addMapper({
  88. filter: addQueryRowAction,
  89. mapper: (state, action): ExploreItemState => {
  90. const { queries, queryTransactions } = state;
  91. const { index, query } = action.payload;
  92. // Add to queries, which will cause a new row to be rendered
  93. const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)];
  94. // Ongoing transactions need to update their row indices
  95. const nextQueryTransactions = queryTransactions.map(qt => {
  96. if (qt.rowIndex > index) {
  97. return {
  98. ...qt,
  99. rowIndex: qt.rowIndex + 1,
  100. };
  101. }
  102. return qt;
  103. });
  104. return {
  105. ...state,
  106. queries: nextQueries,
  107. logsHighlighterExpressions: undefined,
  108. queryTransactions: nextQueryTransactions,
  109. queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
  110. };
  111. },
  112. })
  113. .addMapper({
  114. filter: changeQueryAction,
  115. mapper: (state, action): ExploreItemState => {
  116. const { queries, queryTransactions } = state;
  117. const { query, index } = action.payload;
  118. // Override path: queries are completely reset
  119. const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) };
  120. const nextQueries = [...queries];
  121. nextQueries[index] = nextQuery;
  122. // Discard ongoing transaction related to row query
  123. const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
  124. return {
  125. ...state,
  126. queries: nextQueries,
  127. queryTransactions: nextQueryTransactions,
  128. queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
  129. };
  130. },
  131. })
  132. .addMapper({
  133. filter: changeSizeAction,
  134. mapper: (state, action): ExploreItemState => {
  135. const { range, datasourceInstance } = state;
  136. let interval = '1s';
  137. if (datasourceInstance && datasourceInstance.interval) {
  138. interval = datasourceInstance.interval;
  139. }
  140. const containerWidth = action.payload.width;
  141. const queryIntervals = getIntervals(range, interval, containerWidth);
  142. return { ...state, containerWidth, queryIntervals };
  143. },
  144. })
  145. .addMapper({
  146. filter: changeTimeAction,
  147. mapper: (state, action): ExploreItemState => {
  148. return { ...state, range: action.payload.range };
  149. },
  150. })
  151. .addMapper({
  152. filter: clearQueriesAction,
  153. mapper: (state): ExploreItemState => {
  154. const queries = ensureQueries();
  155. return {
  156. ...state,
  157. queries: queries.slice(),
  158. queryTransactions: [],
  159. showingStartPage: Boolean(state.StartPage),
  160. queryKeys: getQueryKeys(queries, state.datasourceInstance),
  161. };
  162. },
  163. })
  164. .addMapper({
  165. filter: highlightLogsExpressionAction,
  166. mapper: (state, action): ExploreItemState => {
  167. const { expressions } = action.payload;
  168. return { ...state, logsHighlighterExpressions: expressions };
  169. },
  170. })
  171. .addMapper({
  172. filter: initializeExploreAction,
  173. mapper: (state, action): ExploreItemState => {
  174. const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
  175. return {
  176. ...state,
  177. containerWidth,
  178. eventBridge,
  179. exploreDatasources,
  180. range,
  181. queries,
  182. initialized: true,
  183. queryKeys: getQueryKeys(queries, state.datasourceInstance),
  184. ...ui,
  185. };
  186. },
  187. })
  188. .addMapper({
  189. filter: updateDatasourceInstanceAction,
  190. mapper: (state, action): ExploreItemState => {
  191. const { datasourceInstance } = action.payload;
  192. return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.queries, datasourceInstance) };
  193. },
  194. })
  195. .addMapper({
  196. filter: loadDatasourceFailureAction,
  197. mapper: (state, action): ExploreItemState => {
  198. return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
  199. },
  200. })
  201. .addMapper({
  202. filter: loadDatasourceMissingAction,
  203. mapper: (state): ExploreItemState => {
  204. return { ...state, datasourceMissing: true, datasourceLoading: false };
  205. },
  206. })
  207. .addMapper({
  208. filter: loadDatasourcePendingAction,
  209. mapper: (state, action): ExploreItemState => {
  210. return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName };
  211. },
  212. })
  213. .addMapper({
  214. filter: loadDatasourceSuccessAction,
  215. mapper: (state, action): ExploreItemState => {
  216. const { containerWidth, range } = state;
  217. const {
  218. StartPage,
  219. datasourceInstance,
  220. history,
  221. showingStartPage,
  222. supportsGraph,
  223. supportsLogs,
  224. supportsTable,
  225. } = action.payload;
  226. const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth);
  227. return {
  228. ...state,
  229. queryIntervals,
  230. StartPage,
  231. datasourceInstance,
  232. history,
  233. showingStartPage,
  234. supportsGraph,
  235. supportsLogs,
  236. supportsTable,
  237. datasourceLoading: false,
  238. datasourceMissing: false,
  239. datasourceError: null,
  240. logsHighlighterExpressions: undefined,
  241. queryTransactions: [],
  242. };
  243. },
  244. })
  245. .addMapper({
  246. filter: modifyQueriesAction,
  247. mapper: (state, action): ExploreItemState => {
  248. const { queries, queryTransactions } = state;
  249. const { modification, index, modifier } = action.payload;
  250. let nextQueries: DataQuery[];
  251. let nextQueryTransactions;
  252. if (index === undefined) {
  253. // Modify all queries
  254. nextQueries = queries.map((query, i) => ({
  255. ...modifier({ ...query }, modification),
  256. ...generateEmptyQuery(i),
  257. }));
  258. // Discard all ongoing transactions
  259. nextQueryTransactions = [];
  260. } else {
  261. // Modify query only at index
  262. nextQueries = queries.map((query, i) => {
  263. // Synchronize all queries with local query cache to ensure consistency
  264. // TODO still needed?
  265. return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query;
  266. });
  267. nextQueryTransactions = queryTransactions
  268. // Consume the hint corresponding to the action
  269. .map(qt => {
  270. if (qt.hints != null && qt.rowIndex === index) {
  271. qt.hints = qt.hints.filter(hint => hint.fix.action !== modification);
  272. }
  273. return qt;
  274. })
  275. // Preserve previous row query transaction to keep results visible if next query is incomplete
  276. .filter(qt => modification.preventSubmit || qt.rowIndex !== index);
  277. }
  278. return {
  279. ...state,
  280. queries: nextQueries,
  281. queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
  282. queryTransactions: nextQueryTransactions,
  283. };
  284. },
  285. })
  286. .addMapper({
  287. filter: queryTransactionFailureAction,
  288. mapper: (state, action): ExploreItemState => {
  289. const { queryTransactions } = action.payload;
  290. return { ...state, queryTransactions, showingStartPage: false };
  291. },
  292. })
  293. .addMapper({
  294. filter: queryTransactionStartAction,
  295. mapper: (state, action): ExploreItemState => {
  296. const { queryTransactions } = state;
  297. const { resultType, rowIndex, transaction } = action.payload;
  298. // Discarding existing transactions of same type
  299. const remainingTransactions = queryTransactions.filter(
  300. qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex)
  301. );
  302. // Append new transaction
  303. const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
  304. return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false };
  305. },
  306. })
  307. .addMapper({
  308. filter: queryTransactionSuccessAction,
  309. mapper: (state, action): ExploreItemState => {
  310. const { datasourceInstance, queryIntervals } = state;
  311. const { history, queryTransactions } = action.payload;
  312. const results = calculateResultsFromQueryTransactions(
  313. queryTransactions,
  314. datasourceInstance,
  315. queryIntervals.intervalMs
  316. );
  317. return { ...state, ...results, history, queryTransactions, showingStartPage: false };
  318. },
  319. })
  320. .addMapper({
  321. filter: removeQueryRowAction,
  322. mapper: (state, action): ExploreItemState => {
  323. const { datasourceInstance, queries, queryIntervals, queryTransactions, queryKeys } = state;
  324. const { index } = action.payload;
  325. if (queries.length <= 1) {
  326. return state;
  327. }
  328. const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
  329. const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)];
  330. // Discard transactions related to row query
  331. const nextQueryTransactions = queryTransactions.filter(qt => nextQueries.some(nq => nq.key === qt.query.key));
  332. const results = calculateResultsFromQueryTransactions(
  333. nextQueryTransactions,
  334. datasourceInstance,
  335. queryIntervals.intervalMs
  336. );
  337. return {
  338. ...state,
  339. ...results,
  340. queries: nextQueries,
  341. logsHighlighterExpressions: undefined,
  342. queryTransactions: nextQueryTransactions,
  343. queryKeys: nextQueryKeys,
  344. };
  345. },
  346. })
  347. .addMapper({
  348. filter: runQueriesEmptyAction,
  349. mapper: (state): ExploreItemState => {
  350. return { ...state, queryTransactions: [] };
  351. },
  352. })
  353. .addMapper({
  354. filter: scanRangeAction,
  355. mapper: (state, action): ExploreItemState => {
  356. return { ...state, scanRange: action.payload.range };
  357. },
  358. })
  359. .addMapper({
  360. filter: scanStartAction,
  361. mapper: (state, action): ExploreItemState => {
  362. return { ...state, scanning: true, scanner: action.payload.scanner };
  363. },
  364. })
  365. .addMapper({
  366. filter: scanStopAction,
  367. mapper: (state): ExploreItemState => {
  368. const { queryTransactions } = state;
  369. const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
  370. return {
  371. ...state,
  372. queryTransactions: nextQueryTransactions,
  373. scanning: false,
  374. scanRange: undefined,
  375. scanner: undefined,
  376. };
  377. },
  378. })
  379. .addMapper({
  380. filter: setQueriesAction,
  381. mapper: (state, action): ExploreItemState => {
  382. const { queries } = action.payload;
  383. return {
  384. ...state,
  385. queries: queries.slice(),
  386. queryKeys: getQueryKeys(queries, state.datasourceInstance),
  387. };
  388. },
  389. })
  390. .addMapper({
  391. filter: updateUIStateAction,
  392. mapper: (state, action): ExploreItemState => {
  393. return { ...state, ...action.payload };
  394. },
  395. })
  396. .addMapper({
  397. filter: toggleGraphAction,
  398. mapper: (state): ExploreItemState => {
  399. const showingGraph = !state.showingGraph;
  400. let nextQueryTransactions = state.queryTransactions;
  401. if (!showingGraph) {
  402. // Discard transactions related to Graph query
  403. nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
  404. }
  405. return { ...state, queryTransactions: nextQueryTransactions };
  406. },
  407. })
  408. .addMapper({
  409. filter: toggleLogsAction,
  410. mapper: (state): ExploreItemState => {
  411. const showingLogs = !state.showingLogs;
  412. let nextQueryTransactions = state.queryTransactions;
  413. if (!showingLogs) {
  414. // Discard transactions related to Logs query
  415. nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
  416. }
  417. return { ...state, queryTransactions: nextQueryTransactions };
  418. },
  419. })
  420. .addMapper({
  421. filter: toggleTableAction,
  422. mapper: (state): ExploreItemState => {
  423. const showingTable = !state.showingTable;
  424. if (showingTable) {
  425. return { ...state, queryTransactions: state.queryTransactions };
  426. }
  427. // Toggle off needs discarding of table queries and results
  428. const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table');
  429. const results = calculateResultsFromQueryTransactions(
  430. nextQueryTransactions,
  431. state.datasourceInstance,
  432. state.queryIntervals.intervalMs
  433. );
  434. return { ...state, ...results, queryTransactions: nextQueryTransactions };
  435. },
  436. })
  437. .addMapper({
  438. filter: queriesImportedAction,
  439. mapper: (state, action): ExploreItemState => {
  440. const { queries } = action.payload;
  441. return {
  442. ...state,
  443. queries,
  444. queryKeys: getQueryKeys(queries, state.datasourceInstance),
  445. };
  446. },
  447. })
  448. .create();
  449. /**
  450. * Global Explore reducer that handles multiple Explore areas (left and right).
  451. * Actions that have an `exploreId` get routed to the ExploreItemReducer.
  452. */
  453. export const exploreReducer = (state = initialExploreState, action: HigherOrderAction): ExploreState => {
  454. switch (action.type) {
  455. case ActionTypes.SplitClose: {
  456. return { ...state, split: false };
  457. }
  458. case ActionTypes.SplitOpen: {
  459. return { ...state, split: true, right: action.payload.itemState };
  460. }
  461. case ActionTypes.InitializeExploreSplit: {
  462. return { ...state, split: true };
  463. }
  464. case ActionTypes.ResetExplore: {
  465. return initialExploreState;
  466. }
  467. }
  468. if (action.payload) {
  469. const { exploreId } = action.payload as any;
  470. if (exploreId !== undefined) {
  471. const exploreItemState = state[exploreId];
  472. return { ...state, [exploreId]: itemReducer(exploreItemState, action) };
  473. }
  474. }
  475. return state;
  476. };
  477. export default {
  478. explore: exploreReducer,
  479. };