root.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. /* eslint-disable import/no-webpack-loader-syntax, import/no-unresolved */
  2. import debug from 'debug';
  3. import 'moment/locale/zh-cn'
  4. moment.locale('zh-cn');
  5. import moment from 'moment';
  6. import {
  7. size, each, includes, isEqual
  8. } from 'lodash';
  9. import {
  10. fromJS,
  11. is as isDeepEqual,
  12. List as makeList,
  13. Map as makeMap,
  14. OrderedMap as makeOrderedMap,
  15. } from 'immutable';
  16. import ActionTypes from '../constants/action-types';
  17. import {
  18. GRAPH_VIEW_MODE,
  19. TABLE_VIEW_MODE,
  20. } from '../constants/naming';
  21. import {
  22. graphExceedsComplexityThreshSelector,
  23. isResourceViewModeSelector,
  24. } from '../selectors/topology';
  25. import { isPausedSelector } from '../selectors/time-travel';
  26. import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming';
  27. import { applyPinnedSearches } from '../utils/search-utils';
  28. import {
  29. findTopologyById,
  30. setTopologyUrlsById,
  31. updateTopologyIds,
  32. filterHiddenTopologies,
  33. addTopologyFullname,
  34. getDefaultTopology,
  35. } from '../utils/topology-utils';
  36. const log = debug('scope:app-store');
  37. const error = debug('scope:error');
  38. // Helpers
  39. const topologySorter = topology => topology.get('rank');
  40. // Initial values
  41. export const initialState = makeMap({
  42. capabilities: makeMap(),
  43. contrastMode: false,
  44. controlPipes: makeOrderedMap(), // pipeId -> controlPipe
  45. controlStatus: makeMap(),
  46. currentTopology: null,
  47. currentTopologyId: null,
  48. errorUrl: null,
  49. exportingGraph: false,
  50. forceRelayout: false,
  51. gridSortedBy: null,
  52. gridSortedDesc: null,
  53. hostname: '...',
  54. hoveredMetricType: null,
  55. initialNodesLoaded: false,
  56. mouseOverEdgeId: null,
  57. mouseOverNodeId: null,
  58. nodeDetails: makeOrderedMap(), // nodeId -> details
  59. nodes: makeOrderedMap(), // nodeId -> node
  60. // nodes cache, infrequently updated, used for search & resource view
  61. // topologyId -> nodes
  62. nodesByTopology: makeMap(),
  63. nodesLoaded: false,
  64. // class of metric, e.g. 'cpu', rather than 'host_cpu' or 'process_cpu'.
  65. // allows us to keep the same metric "type" selected when the topology changes.
  66. pausedAt: null,
  67. pinnedMetricType: null,
  68. pinnedNetwork: null,
  69. // list of node filters
  70. pinnedSearches: makeList(),
  71. plugins: makeList(),
  72. routeSet: false,
  73. searchFocused: false,
  74. searchQuery: '',
  75. selectedNetwork: null,
  76. selectedNodeId: null,
  77. showingHelp: false,
  78. showingNetworks: false,
  79. showingTroubleshootingMenu: false,
  80. storeViewState: true,
  81. timeTravelTransitioning: false,
  82. topologies: makeList(),
  83. topologiesLoaded: false,
  84. topologyOptions: makeOrderedMap(), // topologyId -> options
  85. topologyUrlsById: makeOrderedMap(), // topologyId -> topologyUrl
  86. topologyViewMode: GRAPH_VIEW_MODE,
  87. version: null,
  88. versionUpdate: null,
  89. // Set some initial numerical values to prevent NaN in case of edgy race conditions.
  90. viewport: makeMap({ height: 0, width: 0 }),
  91. websocketClosed: false,
  92. zoomCache: makeMap(),
  93. });
  94. function calcSelectType(topology) {
  95. const result = {
  96. ...topology,
  97. options: topology.options && topology.options.map((option) => {
  98. // Server doesn't return the `selectType` key unless the option is something other than `one`.
  99. // Default to `one` if undefined, so the component doesn't have to handle this.
  100. option.selectType = option.selectType || 'one';
  101. return option;
  102. })
  103. };
  104. if (topology.sub_topologies) {
  105. result.sub_topologies = topology.sub_topologies.map(calcSelectType);
  106. }
  107. return result;
  108. }
  109. // adds ID field to topology (based on last part of URL path) and save urls in
  110. // map for easy lookup
  111. function processTopologies(state, nextTopologies) {
  112. // add IDs to topology objects in-place
  113. const topologiesWithId = updateTopologyIds(nextTopologies);
  114. // filter out hidden topos
  115. const visibleTopologies = filterHiddenTopologies(topologiesWithId, state.get('currentTopology'));
  116. // set `selectType` field for topology and sub_topologies options (recursive).
  117. const topologiesWithSelectType = visibleTopologies.map(calcSelectType);
  118. // cache URLs by ID
  119. state = state.set(
  120. 'topologyUrlsById',
  121. setTopologyUrlsById(state.get('topologyUrlsById'), topologiesWithSelectType)
  122. );
  123. const topologiesWithFullnames = addTopologyFullname(topologiesWithSelectType);
  124. const immNextTopologies = fromJS(topologiesWithFullnames).sortBy(topologySorter);
  125. return state.set('topologies', immNextTopologies);
  126. }
  127. function setTopology(state, topologyId) {
  128. state = state.set('currentTopology', findTopologyById(state.get('topologies'), topologyId));
  129. return state.set('currentTopologyId', topologyId);
  130. }
  131. export function getDefaultTopologyOptions(state) {
  132. let topologyOptions = makeOrderedMap();
  133. state.get('topologies').forEach((topology) => {
  134. let defaultOptions = makeOrderedMap();
  135. if (topology.has('options') && topology.get('options')) {
  136. topology.get('options').forEach((option) => {
  137. const optionId = option.get('id');
  138. const defaultValue = option.get('defaultValue');
  139. defaultOptions = defaultOptions.set(optionId, [defaultValue]);
  140. });
  141. }
  142. if (defaultOptions.size) {
  143. topologyOptions = topologyOptions.set(topology.get('id'), defaultOptions);
  144. }
  145. });
  146. return topologyOptions;
  147. }
  148. function closeNodeDetails(state, nodeId) {
  149. const nodeDetails = state.get('nodeDetails');
  150. if (nodeDetails.size > 0) {
  151. const popNodeId = nodeId || nodeDetails.keySeq().last();
  152. // remove pipe if it belongs to the node being closed
  153. state = state.update(
  154. 'controlPipes',
  155. controlPipes => controlPipes.filter(pipe => pipe.get('nodeId') !== popNodeId)
  156. );
  157. state = state.deleteIn(['nodeDetails', popNodeId]);
  158. }
  159. if (state.get('nodeDetails').size === 0 || state.get('selectedNodeId') === nodeId) {
  160. state = state.set('selectedNodeId', null);
  161. }
  162. return state;
  163. }
  164. function closeAllNodeDetails(state) {
  165. while (state.get('nodeDetails').size) {
  166. state = closeNodeDetails(state);
  167. }
  168. return state;
  169. }
  170. function clearNodes(state) {
  171. return state
  172. .update('nodes', nodes => nodes.clear())
  173. .set('nodesLoaded', false);
  174. }
  175. // TODO: These state changes should probably be calculated from selectors.
  176. function updateStateFromNodes(state) {
  177. // Apply pinned searches, filters nodes that dont match.
  178. state = applyPinnedSearches(state);
  179. // In case node or edge disappears before mouseleave event.
  180. const nodesIds = state.get('nodes').keySeq();
  181. if (!nodesIds.contains(state.get('mouseOverNodeId'))) {
  182. state = state.set('mouseOverNodeId', null);
  183. }
  184. if (!nodesIds.some(nodeId => includes(state.get('mouseOverEdgeId'), nodeId))) {
  185. state = state.set('mouseOverEdgeId', null);
  186. }
  187. // Update the nodes cache only if we're not in the resource view mode, as we
  188. // intentionally want to keep it static before we figure how to keep it up-to-date.
  189. if (!isResourceViewModeSelector(state)) {
  190. const nodesForCurrentTopologyKey = ['nodesByTopology', state.get('currentTopologyId')];
  191. state = state.setIn(nodesForCurrentTopologyKey, state.get('nodes'));
  192. }
  193. // Clear the error.
  194. state = state.set('errorUrl', null);
  195. return state;
  196. }
  197. export function rootReducer(state = initialState, action) {
  198. if (!action.type) {
  199. error('Payload missing a type!', action);
  200. }
  201. switch (action.type) {
  202. case ActionTypes.BLUR_SEARCH: {
  203. return state.set('searchFocused', false);
  204. }
  205. case ActionTypes.FOCUS_SEARCH: {
  206. return state.set('searchFocused', true);
  207. }
  208. case ActionTypes.CHANGE_TOPOLOGY_OPTION: {
  209. // set option on parent topology
  210. const topology = findTopologyById(state.get('topologies'), action.topologyId);
  211. if (topology) {
  212. const topologyId = topology.get('parentId') || topology.get('id');
  213. const optionKey = ['topologyOptions', topologyId, action.option];
  214. const currentOption = state.getIn(optionKey);
  215. if (!isEqual(currentOption, action.value)) {
  216. state = clearNodes(state);
  217. }
  218. state = state.setIn(optionKey, action.value);
  219. }
  220. return state;
  221. }
  222. case ActionTypes.SET_VIEWPORT_DIMENSIONS: {
  223. return state.mergeIn(['viewport'], {
  224. height: action.height,
  225. width: action.width,
  226. });
  227. }
  228. case ActionTypes.SET_EXPORTING_GRAPH: {
  229. return state.set('exportingGraph', action.exporting);
  230. }
  231. case ActionTypes.SORT_ORDER_CHANGED: {
  232. return state.merge({
  233. gridSortedBy: action.sortedBy,
  234. gridSortedDesc: action.sortedDesc,
  235. });
  236. }
  237. case ActionTypes.SET_VIEW_MODE: {
  238. return state.set('topologyViewMode', action.viewMode);
  239. }
  240. case ActionTypes.CACHE_ZOOM_STATE: {
  241. return state.setIn(activeTopologyZoomCacheKeyPathSelector(state), action.zoomState);
  242. }
  243. case ActionTypes.CLEAR_CONTROL_ERROR: {
  244. return state.removeIn(['controlStatus', action.nodeId, 'error']);
  245. }
  246. case ActionTypes.CLICK_BACKGROUND: {
  247. if (state.get('showingHelp')) {
  248. state = state.set('showingHelp', false);
  249. }
  250. if (state.get('showingTroubleshootingMenu')) {
  251. state = state.set('showingTroubleshootingMenu', false);
  252. }
  253. return closeAllNodeDetails(state);
  254. }
  255. case ActionTypes.CLICK_CLOSE_DETAILS: {
  256. return closeNodeDetails(state, action.nodeId);
  257. }
  258. case ActionTypes.CLOSE_TERMINAL: {
  259. return state.update('controlPipes', controlPipes => controlPipes.clear());
  260. }
  261. case ActionTypes.CLICK_FORCE_RELAYOUT: {
  262. return state.set('forceRelayout', action.forceRelayout);
  263. }
  264. case ActionTypes.CLICK_NODE: {
  265. const prevSelectedNodeId = state.get('selectedNodeId');
  266. const prevDetailsStackSize = state.get('nodeDetails').size;
  267. // click on sibling closes all
  268. state = closeAllNodeDetails(state);
  269. state.set('nodeShape', action.origin)
  270. // select new node if it's not the same (in that case just delesect)
  271. if (prevDetailsStackSize > 1 || prevSelectedNodeId !== action.nodeId) {
  272. // dont set origin if a node was already selected, suppresses animation
  273. const origin = prevSelectedNodeId === null ? action.origin : null;
  274. state = state.setIn(
  275. ['nodeDetails', action.nodeId],
  276. {
  277. id: action.nodeId,
  278. label: action.label,
  279. shape:action.origin,
  280. origin,
  281. topologyId: action.topologyId || state.get('currentTopologyId'),
  282. }
  283. );
  284. state = state.set('selectedNodeId', action.nodeId);
  285. }
  286. return state;
  287. }
  288. case ActionTypes.CLICK_RELATIVE: {
  289. if (state.hasIn(['nodeDetails', action.nodeId])) {
  290. // bring to front
  291. const details = state.getIn(['nodeDetails', action.nodeId]);
  292. state = state.deleteIn(['nodeDetails', action.nodeId]);
  293. state = state.setIn(['nodeDetails', action.nodeId], details);
  294. } else {
  295. state = state.setIn(
  296. ['nodeDetails', action.nodeId],
  297. {
  298. id: action.nodeId,
  299. label: action.label,
  300. origin: action.origin,
  301. topologyId: action.topologyId
  302. }
  303. );
  304. }
  305. return state;
  306. }
  307. case ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE: {
  308. state = state.update(
  309. 'nodeDetails',
  310. nodeDetails => nodeDetails.filter((v, k) => k === action.nodeId)
  311. );
  312. state = state.update('controlPipes', controlPipes => controlPipes.clear());
  313. state = state.set('selectedNodeId', action.nodeId);
  314. if (action.topologyId !== state.get('currentTopologyId')) {
  315. state = setTopology(state, action.topologyId);
  316. state = clearNodes(state);
  317. }
  318. return state;
  319. }
  320. case ActionTypes.CLICK_TOPOLOGY: {
  321. state = closeAllNodeDetails(state);
  322. const currentTopologyId = state.get('currentTopologyId');
  323. if (action.topologyId !== currentTopologyId) {
  324. state = setTopology(state, action.topologyId);
  325. state = clearNodes(state);
  326. }
  327. return state;
  328. }
  329. //
  330. // time control
  331. //
  332. case ActionTypes.RESUME_TIME: {
  333. state = state.set('timeTravelTransitioning', true);
  334. return state.set('pausedAt', null);
  335. }
  336. case ActionTypes.PAUSE_TIME_AT_NOW: {
  337. state = state.set('timeTravelTransitioning', false);
  338. // return state.set('pausedAt', moment().utc().format());
  339. return state.set('pausedAt', moment().utcOffset(8).format())
  340. }
  341. case ActionTypes.JUMP_TO_TIME: {
  342. state = state.set('timeTravelTransitioning', true);
  343. return state.set('pausedAt', action.timestamp);
  344. }
  345. case ActionTypes.FINISH_TIME_TRAVEL_TRANSITION: {
  346. state = state.set('timeTravelTransitioning', false);
  347. return clearNodes(state);
  348. }
  349. //
  350. // websockets
  351. //
  352. case ActionTypes.OPEN_WEBSOCKET: {
  353. return state.set('websocketClosed', false);
  354. }
  355. case ActionTypes.CLOSE_WEBSOCKET: {
  356. return state.set('websocketClosed', true);
  357. }
  358. //
  359. // networks
  360. //
  361. case ActionTypes.SHOW_NETWORKS: {
  362. if (!action.visible) {
  363. state = state.set('selectedNetwork', null);
  364. state = state.set('pinnedNetwork', null);
  365. }
  366. return state.set('showingNetworks', action.visible);
  367. }
  368. case ActionTypes.SELECT_NETWORK: {
  369. return state.set('selectedNetwork', action.networkId);
  370. }
  371. case ActionTypes.PIN_NETWORK: {
  372. return state.merge({
  373. pinnedNetwork: action.networkId,
  374. selectedNetwork: action.networkId
  375. });
  376. }
  377. case ActionTypes.UNPIN_NETWORK: {
  378. return state.merge({
  379. pinnedNetwork: null,
  380. });
  381. }
  382. //
  383. // metrics
  384. //
  385. case ActionTypes.HOVER_METRIC: {
  386. return state.set('hoveredMetricType', action.metricType);
  387. }
  388. case ActionTypes.UNHOVER_METRIC: {
  389. return state.set('hoveredMetricType', null);
  390. }
  391. case ActionTypes.PIN_METRIC: {
  392. return state.set('pinnedMetricType', action.metricType);
  393. }
  394. case ActionTypes.UNPIN_METRIC: {
  395. return state.set('pinnedMetricType', null);
  396. }
  397. case ActionTypes.SHOW_HELP: {
  398. return state.set('showingHelp', true);
  399. }
  400. case ActionTypes.HIDE_HELP: {
  401. return state.set('showingHelp', false);
  402. }
  403. case ActionTypes.DESELECT_NODE: {
  404. return closeNodeDetails(state);
  405. }
  406. case ActionTypes.DO_CONTROL: {
  407. return state.setIn(['controlStatus', action.nodeId], makeMap({
  408. error: null,
  409. pending: true
  410. }));
  411. }
  412. case ActionTypes.ENTER_EDGE: {
  413. return state.set('mouseOverEdgeId', action.edgeId);
  414. }
  415. case ActionTypes.ENTER_NODE: {
  416. return state.set('mouseOverNodeId', action.nodeId);
  417. }
  418. case ActionTypes.LEAVE_EDGE: {
  419. return state.set('mouseOverEdgeId', null);
  420. }
  421. case ActionTypes.LEAVE_NODE: {
  422. return state.set('mouseOverNodeId', null);
  423. }
  424. case ActionTypes.DO_CONTROL_ERROR: {
  425. return state.setIn(['controlStatus', action.nodeId], makeMap({
  426. error: action.error,
  427. pending: false
  428. }));
  429. }
  430. case ActionTypes.DO_CONTROL_SUCCESS: {
  431. return state.setIn(['controlStatus', action.nodeId], makeMap({
  432. error: null,
  433. pending: false
  434. }));
  435. }
  436. case ActionTypes.UPDATE_SEARCH: {
  437. state = state.set('pinnedSearches', makeList(action.pinnedSearches));
  438. state = state.set('searchQuery', action.searchQuery || '');
  439. return applyPinnedSearches(state);
  440. }
  441. case ActionTypes.RECEIVE_CONTROL_NODE_REMOVED: {
  442. return closeNodeDetails(state, action.nodeId);
  443. }
  444. case ActionTypes.RECEIVE_CONTROL_PIPE: {
  445. return state.setIn(['controlPipes', action.pipeId], makeOrderedMap({
  446. control: action.control,
  447. id: action.pipeId,
  448. nodeId: action.nodeId,
  449. raw: action.rawTty,
  450. resizeTtyControl: action.resizeTtyControl
  451. }));
  452. }
  453. case ActionTypes.RECEIVE_CONTROL_PIPE_STATUS: {
  454. if (state.hasIn(['controlPipes', action.pipeId])) {
  455. state = state.setIn(['controlPipes', action.pipeId, 'status'], action.status);
  456. }
  457. return state;
  458. }
  459. case ActionTypes.RECEIVE_ERROR: {
  460. if (state.get('errorUrl') !== null) {
  461. state = state.set('errorUrl', action.errorUrl);
  462. }
  463. return state;
  464. }
  465. case ActionTypes.RECEIVE_NODE_DETAILS: {
  466. // Ignore the update if paused and the timestamp didn't change.
  467. const setTimestamp = state.getIn(['nodeDetails', action.details.id, 'timestamp']);
  468. if (isPausedSelector(state) && action.requestTimestamp === setTimestamp) {
  469. return state;
  470. }
  471. state = state.set('errorUrl', null);
  472. // disregard if node is not selected anymore
  473. if (state.hasIn(['nodeDetails', action.details.id])) {
  474. state = state.updateIn(['nodeDetails', action.details.id], obj => ({
  475. ...obj,
  476. details: action.details,
  477. notFound: false,
  478. timestamp: action.requestTimestamp,
  479. }));
  480. }
  481. return state;
  482. }
  483. case ActionTypes.SET_RECEIVED_NODES_DELTA: {
  484. // Turn on the table view if the graph is too complex, but skip
  485. // this block if the user has already loaded topologies once.
  486. if (!state.get('initialNodesLoaded') && !state.get('nodesLoaded')) {
  487. if (state.get('topologyViewMode') === GRAPH_VIEW_MODE) {
  488. state = graphExceedsComplexityThreshSelector(state)
  489. ? state.set('topologyViewMode', TABLE_VIEW_MODE) : state;
  490. }
  491. state = state.set('initialNodesLoaded', true);
  492. }
  493. return state.set('nodesLoaded', true);
  494. }
  495. case ActionTypes.RECEIVE_NODES_DELTA: {
  496. // Ignore periodic nodes updates after the first load when paused.
  497. if (state.get('nodesLoaded') && state.get('pausedAt')) {
  498. return state;
  499. }
  500. log(
  501. 'RECEIVE_NODES_DELTA',
  502. 'remove', size(action.delta.remove),
  503. 'update', size(action.delta.update),
  504. 'add', size(action.delta.add),
  505. 'reset', action.delta.reset
  506. );
  507. if (action.delta.reset) {
  508. state = state.set('nodes', makeMap());
  509. }
  510. // remove nodes that no longer exist
  511. each(action.delta.remove, (nodeId) => {
  512. state = state.deleteIn(['nodes', nodeId]);
  513. });
  514. // update existing nodes
  515. each(action.delta.update, (node) => {
  516. if (state.hasIn(['nodes', node.id])) {
  517. // TODO: Implement a manual deep update here, as it might bring a great benefit
  518. // to our nodes selectors (e.g. layout engine would be completely bypassed if the
  519. // adjacencies would stay the same but the metrics would get updated).
  520. state = state.setIn(['nodes', node.id], fromJS(node));
  521. }
  522. });
  523. // add new nodes
  524. each(action.delta.add, (node) => {
  525. state = state.setIn(['nodes', node.id], fromJS(node));
  526. });
  527. return updateStateFromNodes(state);
  528. }
  529. case ActionTypes.RECEIVE_NODES: {
  530. state = state.set('timeTravelTransitioning', false);
  531. state = state.set('nodes', fromJS(action.nodes));
  532. state = state.set('nodesLoaded', true);
  533. return updateStateFromNodes(state);
  534. }
  535. case ActionTypes.RECEIVE_NODES_FOR_TOPOLOGY: {
  536. return state.setIn(['nodesByTopology', action.topologyId], fromJS(action.nodes));
  537. }
  538. case ActionTypes.RECEIVE_NOT_FOUND: {
  539. if (state.hasIn(['nodeDetails', action.nodeId])) {
  540. state = state.updateIn(['nodeDetails', action.nodeId], obj => ({
  541. ...obj,
  542. notFound: true,
  543. timestamp: action.requestTimestamp,
  544. }));
  545. }
  546. return state;
  547. }
  548. case ActionTypes.RECEIVE_TOPOLOGIES: {
  549. state = state.set('errorUrl', null);
  550. state = state.update('topologyUrlsById', topologyUrlsById => topologyUrlsById.clear());
  551. state = processTopologies(state, action.topologies);
  552. const currentTopologyId = state.get('currentTopologyId');
  553. if (!currentTopologyId || !findTopologyById(state.get('topologies'), currentTopologyId)) {
  554. state = state.set('currentTopologyId', getDefaultTopology(state.get('topologies')));
  555. log(`Set currentTopologyId to ${state.get('currentTopologyId')}`);
  556. }
  557. state = setTopology(state, state.get('currentTopologyId'));
  558. // Expand topology options with topologies' defaults on first load, but let
  559. // the current state of topologyOptions (which at this point reflects the
  560. // URL state) still take the precedence over defaults.
  561. if (!state.get('topologiesLoaded')) {
  562. const options = getDefaultTopologyOptions(state).mergeDeep(state.get('topologyOptions'));
  563. state = state.set('topologyOptions', options);
  564. state = state.set('topologiesLoaded', true);
  565. }
  566. return state;
  567. }
  568. case ActionTypes.RECEIVE_API_DETAILS: {
  569. state = state.set('errorUrl', null);
  570. return state.merge({
  571. capabilities: action.capabilities,
  572. hostname: action.hostname,
  573. plugins: action.plugins,
  574. version: action.version,
  575. versionUpdate: action.newVersion,
  576. });
  577. }
  578. case ActionTypes.ROUTE_TOPOLOGY: {
  579. state = state.set('routeSet', true);
  580. state = state.set('pinnedSearches', makeList(action.state.pinnedSearches));
  581. state = state.set('searchQuery', action.state.searchQuery || '');
  582. if (state.get('currentTopologyId') !== action.state.topologyId) {
  583. state = clearNodes(state);
  584. }
  585. state = setTopology(state, action.state.topologyId);
  586. state = state.merge({
  587. pinnedMetricType: action.state.pinnedMetricType,
  588. selectedNodeId: action.state.selectedNodeId,
  589. });
  590. if (action.state.topologyOptions) {
  591. const options = getDefaultTopologyOptions(state).mergeDeep(action.state.topologyOptions);
  592. state = state.set('topologyOptions', options);
  593. }
  594. if (action.state.topologyViewMode) {
  595. state = state.set('topologyViewMode', action.state.topologyViewMode);
  596. }
  597. if (action.state.gridSortedBy) {
  598. state = state.set('gridSortedBy', action.state.gridSortedBy);
  599. }
  600. if (action.state.gridSortedDesc !== undefined) {
  601. state = state.set('gridSortedDesc', action.state.gridSortedDesc);
  602. }
  603. if (action.state.contrastMode !== undefined) {
  604. state = state.set('contrastMode', action.state.contrastMode);
  605. }
  606. if (action.state.showingNetworks) {
  607. state = state.set('showingNetworks', action.state.showingNetworks);
  608. }
  609. if (action.state.pinnedNetwork) {
  610. state = state.set('pinnedNetwork', action.state.pinnedNetwork);
  611. state = state.set('selectedNetwork', action.state.pinnedNetwork);
  612. }
  613. if (action.state.controlPipe) {
  614. state = state.set('controlPipes', makeOrderedMap({
  615. [action.state.controlPipe.id]:
  616. makeOrderedMap(action.state.controlPipe)
  617. }));
  618. } else {
  619. state = state.update('controlPipes', controlPipes => controlPipes.clear());
  620. }
  621. if (action.state.nodeDetails) {
  622. const actionNodeDetails = makeOrderedMap(action.state.nodeDetails.map(h => [h.id, h]));
  623. // check if detail IDs have changed
  624. if (!isDeepEqual(state.get('nodeDetails').keySeq(), actionNodeDetails.keySeq())) {
  625. state = state.set('nodeDetails', actionNodeDetails);
  626. }
  627. } else {
  628. state = state.update('nodeDetails', nodeDetails => nodeDetails.clear());
  629. }
  630. return state;
  631. }
  632. case ActionTypes.DEBUG_TOOLBAR_INTERFERING: {
  633. return action.fn(state);
  634. }
  635. case ActionTypes.TOGGLE_TROUBLESHOOTING_MENU: {
  636. return state.set('showingTroubleshootingMenu', !state.get('showingTroubleshootingMenu'));
  637. }
  638. case ActionTypes.CHANGE_INSTANCE: {
  639. state = closeAllNodeDetails(state);
  640. return state;
  641. }
  642. case ActionTypes.TOGGLE_CONTRAST_MODE: {
  643. return state.set('contrastMode', action.enabled);
  644. }
  645. case ActionTypes.SHUTDOWN: {
  646. return clearNodes(state);
  647. }
  648. case ActionTypes.MONITOR_STATE: {
  649. return state.set('monitor', action.monitor);
  650. }
  651. case ActionTypes.SET_STORE_VIEW_STATE: {
  652. return state.set('storeViewState', action.storeViewState);
  653. }
  654. default: {
  655. return state;
  656. }
  657. }
  658. }
  659. export default rootReducer;