reducers.ts 20 KB

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