reducers.ts 15 KB

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