diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index ebbfdaebd..4a468988f 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -3,7 +3,7 @@ import { fromJS } from 'immutable'; import ActionTypes from '../constants/action-types'; import { saveGraph } from '../utils/file-utils'; -import { updateRoute } from '../utils/router-utils'; +import { clearStoredViewState, updateRoute } from '../utils/router-utils'; import { doControlRequest, getAllNodes, @@ -15,7 +15,6 @@ import { teardownWebsockets, getNodes, } from '../utils/web-api-utils'; -import { storageSet } from '../utils/storage-utils'; import { loadTheme } from '../utils/contrast-utils'; import { isPausedSelector } from '../selectors/time-travel'; import { @@ -794,7 +793,7 @@ export function route(urlState) { export function resetLocalViewState() { return (dispatch) => { dispatch({type: ActionTypes.RESET_LOCAL_VIEW_STATE}); - storageSet('scopeViewState', ''); + clearStoredViewState(); // eslint-disable-next-line prefer-destructuring window.location.href = window.location.href.split('#')[0]; }; @@ -832,3 +831,10 @@ export function setMonitorState(monitor) { monitor }; } + +export function setStoreViewState(storeViewState) { + return { + type: ActionTypes.SET_STORE_VIEW_STATE, + storeViewState + }; +} diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 233791066..4588b2419 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -31,6 +31,7 @@ import { setMonitorState, setTableView, setResourceView, + setStoreViewState, shutdown, setViewportDimensions, getTopologiesWithInitialPoll, @@ -64,6 +65,7 @@ class App extends React.Component { super(props, context); this.props.dispatch(setMonitorState(this.props.monitor)); + this.props.dispatch(setStoreViewState(!this.props.disableStoreViewState)); this.setViewportDimensions = this.setViewportDimensions.bind(this); this.handleResize = debounce(this.setViewportDimensions, VIEWPORT_RESIZE_DEBOUNCE_INTERVAL); @@ -79,7 +81,7 @@ class App extends React.Component { window.addEventListener('keypress', this.onKeyPress); window.addEventListener('keyup', this.onKeyUp); - this.router = getRouter(this.props.dispatch, this.props.urlState); + this.router = this.props.dispatch(getRouter(this.props.urlState)); this.router.start({ hashbang: true }); if (!this.props.routeSet || process.env.WEAVE_CLOUD) { @@ -102,6 +104,9 @@ class App extends React.Component { if (nextProps.monitor !== this.props.monitor) { this.props.dispatch(setMonitorState(nextProps.monitor)); } + if (nextProps.disableStoreViewState !== this.props.disableStoreViewState) { + this.props.dispatch(setStoreViewState(!nextProps.disableStoreViewState)); + } } onKeyUp(ev) { @@ -267,12 +272,14 @@ App.propTypes = { renderTimeTravel: PropTypes.func, renderNodeDetailsExtras: PropTypes.func, monitor: PropTypes.bool, + disableStoreViewState: PropTypes.bool, }; App.defaultProps = { renderTimeTravel: () => , renderNodeDetailsExtras: () => null, monitor: false, + disableStoreViewState: false, }; export default connect(mapStateToProps)(App); diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index 3323c862c..47ae93a39 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -55,6 +55,7 @@ const ACTION_TYPES = [ 'SELECT_NETWORK', 'SET_EXPORTING_GRAPH', 'SET_RECEIVED_NODES_DELTA', + 'SET_STORE_VIEW_STATE', 'SET_VIEW_MODE', 'SET_VIEWPORT_DIMENSIONS', 'SHOW_HELP', diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 7ebfee1b1..6ab9b007f 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -70,6 +70,7 @@ export const initialState = makeMap({ plugins: makeList(), pinnedSearches: makeList(), // list of node filters routeSet: false, + storeViewState: true, searchFocused: false, searchQuery: '', selectedNetwork: null, @@ -752,6 +753,10 @@ export function rootReducer(state = initialState, action) { return state.set('monitor', action.monitor); } + case ActionTypes.SET_STORE_VIEW_STATE: { + return state.set('storeViewState', action.storeViewState); + } + default: { return state; } diff --git a/client/app/scripts/utils/router-utils.js b/client/app/scripts/utils/router-utils.js index 244d61a3a..fc4425327 100644 --- a/client/app/scripts/utils/router-utils.js +++ b/client/app/scripts/utils/router-utils.js @@ -37,6 +37,14 @@ export function parseHashState(hash = window.location.hash) { return JSON.parse(decodeURL(urlStateString)); } +export function clearStoredViewState() { + storageSet(STORAGE_STATE_KEY, ''); +} + +function isStoreViewStateEnabled(state) { + return state.get('storeViewState'); +} + function shouldReplaceState(prevState, nextState) { // Opening a new terminal while an existing one is open. const terminalToTerminal = (prevState.controlPipe && nextState.controlPipe); @@ -116,7 +124,9 @@ export function updateRoute(getState) { if (stateUrl === prevStateUrl) return; // back up state in storage as well - storageSet(STORAGE_STATE_KEY, stateUrl); + if (isStoreViewStateEnabled(getState())) { + storageSet(STORAGE_STATE_KEY, stateUrl); + } if (shouldReplaceState(prevState, state)) { // Replace the top of the history rather than pushing on a new item. @@ -141,38 +151,43 @@ function detectOldOptions(topologyOptions) { } -export function getRouter(dispatch, initialState) { - // strip any trailing '/'s. - page.base(window.location.pathname.replace(/\/$/, '')); +export function getRouter(initialState) { + return (dispatch, getState) => { + // strip any trailing '/'s. + page.base(window.location.pathname.replace(/\/$/, '')); - page('/', () => { - // recover from storage state on empty URL - const storageState = storageGet(STORAGE_STATE_KEY); - if (storageState) { - const parsedState = JSON.parse(decodeURL(storageState)); - const dirtyOptions = detectOldOptions(parsedState.topologyOptions); - if (dirtyOptions) { - dispatch(route(initialState)); + page('/', () => { + // recover from storage state on empty URL + const storageState = storageGet(STORAGE_STATE_KEY); + if (storageState && isStoreViewStateEnabled(getState())) { + const parsedState = JSON.parse(decodeURL(storageState)); + const dirtyOptions = detectOldOptions(parsedState.topologyOptions); + if (dirtyOptions) { + dispatch(route(initialState)); + } else { + const mergedState = Object.assign(initialState, parsedState); + // push storage state to URL + window.location.hash = `!/state/${stableStringify(mergedState)}`; + dispatch(route(mergedState)); + } } else { - const mergedState = Object.assign(initialState, parsedState); - // push storage state to URL - window.location.hash = `!/state/${stableStringify(mergedState)}`; - dispatch(route(mergedState)); + dispatch(route(initialState)); } - } else { - dispatch(route(initialState)); - } - }); + }); - page('/state/:state', (ctx) => { - const state = JSON.parse(decodeURL(ctx.params.state)); - const dirtyOptions = detectOldOptions(state.topologyOptions); - const nextState = dirtyOptions ? initialState : state; + page('/state/:state', (ctx) => { + const state = JSON.parse(decodeURL(ctx.params.state)); + const dirtyOptions = detectOldOptions(state.topologyOptions); + const nextState = dirtyOptions ? initialState : state; - // back up state in storage and redirect - storageSet(STORAGE_STATE_KEY, encodeURL(stableStringify(state))); - dispatch(route(nextState)); - }); + // back up state in storage and redirect + if (isStoreViewStateEnabled(getState())) { + storageSet(STORAGE_STATE_KEY, encodeURL(stableStringify(state))); + } - return page; + dispatch(route(nextState)); + }); + + return page; + }; } diff --git a/client/app/scripts/utils/storage-utils.js b/client/app/scripts/utils/storage-utils.js index 4c6d3ee07..61908660b 100644 --- a/client/app/scripts/utils/storage-utils.js +++ b/client/app/scripts/utils/storage-utils.js @@ -2,30 +2,54 @@ import debug from 'debug'; const log = debug('scope:storage-utils'); -// localStorage detection -const storage = (typeof Storage) !== 'undefined' ? window.localStorage : null; - -export function storageGet(key, defaultValue) { - if (storage && storage.getItem(key) !== undefined) { - return storage.getItem(key); +export const localSessionStorage = { + getItem(k) { + return window.sessionStorage.getItem(k) || window.localStorage.getItem(k); + }, + setItem(k, v) { + window.sessionStorage.setItem(k, v); + window.localStorage.setItem(k, v); + }, + clear() { + window.sessionStorage.clear(); + window.localStorage.clear(); } - return defaultValue; +}; + +export function storageGet(key, defaultValue, storage = localSessionStorage) { + if (!storage) { + return defaultValue; + } + + const value = storage.getItem(key); + if (value == null) { + return defaultValue; + } + + return value; } -export function storageSet(key, value) { +export function storageSet(key, value, storage = localSessionStorage) { if (storage) { try { storage.setItem(key, value); return true; } catch (e) { - log('Error storing value in storage. Maybe full? Could not store key.', key); + log( + 'Error storing value in storage. Maybe full? Could not store key.', + key + ); } } return false; } -export function storageGetObject(key, defaultValue) { - const value = storageGet(key); +export function storageGetObject( + key, + defaultValue, + storage = localSessionStorage +) { + const value = storageGet(key, undefined, storage); if (value) { try { return JSON.parse(value); @@ -36,9 +60,9 @@ export function storageGetObject(key, defaultValue) { return defaultValue; } -export function storageSetObject(key, obj) { +export function storageSetObject(key, obj, storage = localSessionStorage) { try { - return storageSet(key, JSON.stringify(obj)); + return storageSet(key, JSON.stringify(obj), storage); } catch (e) { log('Error encoding object for key', key); } diff --git a/client/package.json b/client/package.json index adcdf6a52..354de17e9 100644 --- a/client/package.json +++ b/client/package.json @@ -118,7 +118,7 @@ }, "setupFiles": [ "/test/support/raf.js", - "/test/support/localStorage.js" + "/test/support/storage.js" ], "roots": [ "/app/scripts" diff --git a/client/test/support/localStorage.js b/client/test/support/localStorage.js deleted file mode 100644 index 50cf4b680..000000000 --- a/client/test/support/localStorage.js +++ /dev/null @@ -1,17 +0,0 @@ -const localStorageMock = (function() { - let store = {}; - return { - store, - getItem: function(key) { - return store[key]; - }, - setItem: function(key, value) { - store[key] = value; - }, - clear: function() { - store = {}; - } - }; -})(); -Object.defineProperty(window, 'Storage', { value: localStorageMock }); -Object.defineProperty(window, 'localStorage', { value: localStorageMock }); diff --git a/client/test/support/storage.js b/client/test/support/storage.js new file mode 100644 index 000000000..47af5aeab --- /dev/null +++ b/client/test/support/storage.js @@ -0,0 +1,21 @@ +const makeStorageMock = function () { + let store = {}; + return { + store, + getItem(key) { + return store[key]; + }, + setItem(key, value) { + store[key] = value; + }, + clear() { + store = {}; + } + }; +}; + +const localStorageMock = makeStorageMock(); +const sessionStorageMock = makeStorageMock(); + +Object.defineProperty(window, 'localStorage', { value: localStorageMock }); +Object.defineProperty(window, 'sessionStorage', { value: sessionStorageMock });