mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 17:51:21 +00:00
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:
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
19
client/app/scripts/utils/hash-utils.js
Normal file
19
client/app/scripts/utils/hash-utils.js
Normal 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));
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user