Merge pull request #3030 from weaveworks/1727-remove-default-values-from-url-hash

Remove default values from URL state hash
This commit is contained in:
Filip Barl
2018-01-22 17:04:52 +01:00
committed by GitHub
4 changed files with 78 additions and 27 deletions

View File

@@ -352,7 +352,7 @@ describe('RootReducer', () => {
expect(activeTopologyOptionsSelector(nextState).has('option1')).toBeTruthy();
expect(activeTopologyOptionsSelector(nextState).get('option1')).toBeInstanceOf(Array);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions).toBeUndefined();
// turn on
nextState = reducer(nextState, ChangeTopologyOptionAction);
@@ -362,17 +362,17 @@ describe('RootReducer', () => {
// turn off
nextState = reducer(nextState, ChangeTopologyOptionAction2);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions).toBeUndefined();
// sub-topology should retain main topo options
nextState = reducer(nextState, ClickSubTopologyAction);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions).toBeUndefined();
// other topology w/o options dont return options, but keep in app state
nextState = reducer(nextState, ClickTopology2Action);
expect(activeTopologyOptionsSelector(nextState).size).toEqual(0);
expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions).toBeUndefined();
});
it('adds/removes a topology option', () => {
@@ -433,7 +433,7 @@ describe('RootReducer', () => {
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopologyAction);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions).toBeUndefined();
});
// nodes delta
@@ -479,7 +479,7 @@ describe('RootReducer', () => {
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopologyAction);
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(getUrlState(nextState).selectedNodeId).toEqual(null);
expect(getUrlState(nextState).selectedNodeId).toBeUndefined();
nextState = reducer(nextState, ClickNodeAction);
expect(getUrlState(nextState).selectedNodeId).toEqual('n1');
@@ -487,7 +487,7 @@ describe('RootReducer', () => {
// go back in browsing
RouteAction.state = {topologyId: 'topo1', selectedNodeId: null};
nextState = reducer(nextState, RouteAction);
expect(nextState.get('selectedNodeId')).toBe(null);
expect(nextState.get('selectedNodeId')).toBeNull();
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
});
@@ -497,7 +497,7 @@ describe('RootReducer', () => {
nextState = reducer(nextState, ClickTopologyAction);
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(getUrlState(nextState).selectedNodeId).toEqual(null);
expect(getUrlState(nextState).selectedNodeId).toBeUndefined();
expect(getUrlState(nextState).topologyId).toEqual('topo1');
nextState = reducer(nextState, ClickNodeAction);
@@ -505,7 +505,7 @@ describe('RootReducer', () => {
expect(getUrlState(nextState).topologyId).toEqual('topo1');
nextState = reducer(nextState, ClickSubTopologyAction);
expect(getUrlState(nextState).selectedNodeId).toEqual(null);
expect(getUrlState(nextState).selectedNodeId).toBeUndefined();
expect(getUrlState(nextState).topologyId).toEqual('topo1-grouped');
});

View File

@@ -71,7 +71,7 @@ export const initialState = makeMap({
pinnedSearches: makeList(), // list of node filters
routeSet: false,
searchFocused: false,
searchQuery: null,
searchQuery: '',
selectedNetwork: null,
selectedNodeId: null,
showingHelp: false,
@@ -135,8 +135,9 @@ function setTopology(state, topologyId) {
return state.set('currentTopologyId', topologyId);
}
function setDefaultTopologyOptions(state, topologyList) {
topologyList.forEach((topology) => {
export function getDefaultTopologyOptions(state) {
let topologyOptions = makeOrderedMap();
state.get('topologies').forEach((topology) => {
let defaultOptions = makeOrderedMap();
if (topology.has('options') && topology.get('options')) {
topology.get('options').forEach((option) => {
@@ -145,15 +146,11 @@ function setDefaultTopologyOptions(state, topologyList) {
defaultOptions = defaultOptions.set(optionId, [defaultValue]);
});
}
if (defaultOptions.size) {
state = state.setIn(
['topologyOptions', topology.get('id')],
defaultOptions
);
topologyOptions = topologyOptions.set(topology.get('id'), defaultOptions);
}
});
return state;
return topologyOptions;
}
function closeNodeDetails(state, nodeId) {
@@ -659,7 +656,7 @@ export function rootReducer(state = initialState, action) {
state = setTopology(state, state.get('currentTopologyId'));
// only set on first load, if options are not already set via route
if (!state.get('topologiesLoaded') && state.get('topologyOptions').size === 0) {
state = setDefaultTopologyOptions(state, state.get('topologies'));
state = state.set('topologyOptions', getDefaultTopologyOptions(state));
}
state = state.set('topologiesLoaded', true);
@@ -686,12 +683,13 @@ export function rootReducer(state = initialState, action) {
state = clearNodes(state);
}
state = setTopology(state, action.state.topologyId);
state = setDefaultTopologyOptions(state, state.get('topologies'));
state = state.merge({
selectedNodeId: action.state.selectedNodeId,
pinnedMetricType: action.state.pinnedMetricType
pinnedMetricType: action.state.pinnedMetricType,
});
state = state.set('topologyViewMode', action.state.topologyViewMode);
if (action.state.topologyViewMode) {
state = state.set('topologyViewMode', action.state.topologyViewMode);
}
if (action.state.gridSortedBy) {
state = state.set('gridSortedBy', action.state.gridSortedBy);
}
@@ -722,9 +720,11 @@ export function rootReducer(state = initialState, action) {
} else {
state = state.update('nodeDetails', nodeDetails => nodeDetails.clear());
}
// Use the default topology options for all the fields not
// explicitly listed in the Scope state (URL or local storage).
state = state.set(
'topologyOptions',
fromJS(action.state.topologyOptions) || state.get('topologyOptions')
getDefaultTopologyOptions(state).mergeDeep(action.state.topologyOptions),
);
return state;
}

View File

@@ -0,0 +1,19 @@
import { isPlainObject, mapValues, isEmpty, omitBy } from 'lodash';
export function hashDifferenceDeep(A, B) {
// If the elements have exactly the same content, the difference is an empty object.
// This could fail if the objects are both hashes with different permutation of keys,
// but this case we handle below by digging in recursively.
if (JSON.stringify(A) === JSON.stringify(B)) return {};
// Otherwise, if either element is not a hash, always return the first element
// unchanged as this function only takes difference of hash objects.
if (!isPlainObject(A) || !isPlainObject(B)) return A;
// If both elements are hashes, recursively take the difference by all keys
const rawDiff = mapValues(A, (value, key) => hashDifferenceDeep(value, B[key]));
// ... and filter out all the empty values.
return omitBy(rawDiff, value => isEmpty(value));
}

View File

@@ -1,9 +1,13 @@
import page from 'page';
import { each } from 'lodash';
import { fromJS, is as isDeepEqual } from 'immutable';
import { each, omit, omitBy, isEmpty } from 'lodash';
import { route } from '../actions/app-actions';
import { hashDifferenceDeep } from './hash-utils';
import { storageGet, storageSet } from './storage-utils';
import { getDefaultTopologyOptions, initialState as initialRootState } from '../reducers/root';
//
// page.js won't match the routes below if ":state" has a slash in it, so replace those before we
// load the state into the URL.
@@ -41,11 +45,37 @@ function shouldReplaceState(prevState, nextState) {
return terminalToTerminal || closingTheTerminal;
}
function omitDefaultValues(urlState) {
// A couple of cases which require special handling because their URL state
// default values might be in different format than their Redux defaults.
if (!urlState.controlPipe) {
urlState = omit(urlState, 'controlPipe');
}
if (isEmpty(urlState.nodeDetails)) {
urlState = omit(urlState, 'nodeDetails');
}
if (isEmpty(urlState.topologyOptions)) {
urlState = omit(urlState, 'topologyOptions');
}
// Omit all the fields which match their initial Redux state values.
return omitBy(urlState, (value, key) => (
isDeepEqual(fromJS(value), initialRootState.get(key))
));
}
export function getUrlState(state) {
const cp = state.get('controlPipes').last();
const nodeDetails = state.get('nodeDetails').toIndexedSeq().map(details => ({
id: details.id, label: details.label, topologyId: details.topologyId
id: details.id, topologyId: details.topologyId
}));
// Compress the topologyOptions hash by removing all the default options, to make
// the Scope state string smaller. The default options will always be used as a
// fallback so they don't need to be explicitly mentioned in the state.
const topologyOptionsDiff = hashDifferenceDeep(
state.get('topologyOptions').toJS(),
getDefaultTopologyOptions(state).toJS(),
);
const urlState = {
controlPipe: cp ? cp.toJS() : null,
@@ -59,7 +89,7 @@ export function getUrlState(state) {
gridSortedBy: state.get('gridSortedBy'),
gridSortedDesc: state.get('gridSortedDesc'),
topologyId: state.get('currentTopologyId'),
topologyOptions: state.get('topologyOptions').toJS(), // all options,
topologyOptions: topologyOptionsDiff,
contrastMode: state.get('contrastMode'),
};
@@ -70,7 +100,9 @@ export function getUrlState(state) {
}
}
return urlState;
// We can omit all the fields whose values correspond to their Redux initial
// state, as that state will be used as fallback anyway when entering routes.
return omitDefaultValues(urlState);
}
export function updateRoute(getState) {