router-utils.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import page from 'page';
  2. import stableStringify from 'json-stable-stringify';
  3. import { fromJS, is as isDeepEqual } from 'immutable';
  4. import { omit, omitBy, isEmpty } from 'lodash';
  5. import { hashDifferenceDeep } from './hash-utils';
  6. import { storageSet } from './storage-utils';
  7. import { getDefaultTopologyOptions, initialState as initialRootState } from '../reducers/root';
  8. //
  9. // page.js won't match the routes below if ":state" has a slash in it, so replace those before we
  10. // load the state into the URL.
  11. //
  12. const SLASH = '/';
  13. const SLASH_REPLACEMENT = '<SLASH>';
  14. const PERCENT = '%';
  15. const PERCENT_REPLACEMENT = '<PERCENT>';
  16. export const STORAGE_STATE_KEY = 'scopeViewState';
  17. export function encodeURL(url) {
  18. return url
  19. .replace(new RegExp(PERCENT, 'g'), PERCENT_REPLACEMENT)
  20. .replace(new RegExp(SLASH, 'g'), SLASH_REPLACEMENT);
  21. }
  22. export function decodeURL(url) {
  23. return decodeURIComponent(url.replace(new RegExp(SLASH_REPLACEMENT, 'g'), SLASH))
  24. .replace(new RegExp(PERCENT_REPLACEMENT, 'g'), PERCENT);
  25. }
  26. export function parseHashState(hash = window.location.hash) {
  27. const urlStateString = hash
  28. .replace('#!/state/', '')
  29. .replace('#!/', '') || '{}';
  30. return JSON.parse(decodeURL(urlStateString));
  31. }
  32. export function clearStoredViewState() {
  33. storageSet(STORAGE_STATE_KEY, '');
  34. }
  35. export function isStoreViewStateEnabled(state) {
  36. return state.get('storeViewState');
  37. }
  38. function shouldReplaceState(prevState, nextState) {
  39. // Opening a new terminal while an existing one is open.
  40. const terminalToTerminal = (prevState.controlPipe && nextState.controlPipe);
  41. // Closing a terminal.
  42. const closingTheTerminal = (prevState.controlPipe && !nextState.controlPipe);
  43. return terminalToTerminal || closingTheTerminal;
  44. }
  45. function omitDefaultValues(urlState) {
  46. // A couple of cases which require special handling because their URL state
  47. // default values might be in different format than their Redux defaults.
  48. if (!urlState.controlPipe) {
  49. urlState = omit(urlState, 'controlPipe');
  50. }
  51. if (isEmpty(urlState.nodeDetails)) {
  52. urlState = omit(urlState, 'nodeDetails');
  53. }
  54. if (isEmpty(urlState.topologyOptions)) {
  55. urlState = omit(urlState, 'topologyOptions');
  56. }
  57. // Omit all the fields which match their initial Redux state values.
  58. return omitBy(urlState, (value, key) => (
  59. isDeepEqual(fromJS(value), initialRootState.get(key))
  60. ));
  61. }
  62. export function getUrlState(state) {
  63. const cp = state.get('controlPipes').last();
  64. const nodeDetails = state.get('nodeDetails').toIndexedSeq().map(details => ({
  65. id: details.id, topologyId: details.topologyId
  66. }));
  67. // Compress the topologyOptions hash by removing all the default options, to make
  68. // the Scope state string smaller. The default options will always be used as a
  69. // fallback so they don't need to be explicitly mentioned in the state.
  70. const topologyOptionsDiff = hashDifferenceDeep(
  71. state.get('topologyOptions').toJS(),
  72. getDefaultTopologyOptions(state).toJS(),
  73. );
  74. const urlState = {
  75. contrastMode: state.get('contrastMode'),
  76. controlPipe: cp ? cp.toJS() : null,
  77. gridSortedBy: state.get('gridSortedBy'),
  78. gridSortedDesc: state.get('gridSortedDesc'),
  79. nodeDetails: nodeDetails.toJS(),
  80. pausedAt: state.get('pausedAt'),
  81. pinnedMetricType: state.get('pinnedMetricType'),
  82. pinnedSearches: state.get('pinnedSearches').toJS(),
  83. searchQuery: state.get('searchQuery'),
  84. selectedNodeId: state.get('selectedNodeId'),
  85. topologyId: state.get('currentTopologyId'),
  86. topologyOptions: topologyOptionsDiff,
  87. topologyViewMode: state.get('topologyViewMode'),
  88. };
  89. if (state.get('showingNetworks')) {
  90. urlState.showingNetworks = true;
  91. if (state.get('pinnedNetwork')) {
  92. urlState.pinnedNetwork = state.get('pinnedNetwork');
  93. }
  94. }
  95. // We can omit all the fields whose values correspond to their Redux initial
  96. // state, as that state will be used as fallback anyway when entering routes.
  97. return omitDefaultValues(urlState);
  98. }
  99. export function updateRoute(getState) {
  100. const state = getUrlState(getState());
  101. const prevState = parseHashState();
  102. const dispatch = false;
  103. const stateUrl = encodeURL(stableStringify(state));
  104. const prevStateUrl = encodeURL(stableStringify(prevState));
  105. if (stateUrl === prevStateUrl) return;
  106. // back up state in storage as well
  107. if (isStoreViewStateEnabled(getState())) {
  108. storageSet(STORAGE_STATE_KEY, stateUrl);
  109. }
  110. if (shouldReplaceState(prevState, state)) {
  111. // Replace the top of the history rather than pushing on a new item.
  112. page.replace(`/state/${stateUrl}`, state, dispatch);
  113. } else {
  114. page.show(`/state/${stateUrl}`, state, dispatch);
  115. }
  116. }