reducers.ts 20 KB

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