reducers.ts 21 KB

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