reducers.ts 20 KB

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