reducers.ts 20 KB

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