diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index a5a9024ee..89b44722a 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -5,14 +5,23 @@ import { saveGraph } from '../utils/file-utils'; import { modulo } from '../utils/math-utils'; import { updateRoute } from '../utils/router-utils'; import { parseQuery } from '../utils/search-utils'; -import { bufferDeltaUpdate, resumeUpdate, - resetUpdateBuffer } from '../utils/update-buffer-utils'; -import { doControlRequest, getAllNodes, getNodesDelta, getNodeDetails, - getTopologies, deletePipe } from '../utils/web-api-utils'; -import { getActiveTopologyOptions, - getCurrentTopologyUrl } from '../utils/topology-utils'; +import { + bufferDeltaUpdate, + resumeUpdate, + resetUpdateBuffer, +} from '../utils/update-buffer-utils'; +import { + doControlRequest, + getAllNodes, + getNodesDelta, + getNodeDetails, + getTopologies, + deletePipe, +} from '../utils/web-api-utils'; +import { getCurrentTopologyUrl } from '../utils/topology-utils'; import { storageSet } from '../utils/storage-utils'; import { loadTheme } from '../utils/contrast-utils'; +import { activeTopologyOptionsSelector } from '../selectors/topology'; const log = debug('scope:app-actions'); @@ -165,16 +174,16 @@ export function changeTopologyOption(option, value, topologyId) { // update all request workers with new options resetUpdateBuffer(); const state = getState(); - getTopologies(getActiveTopologyOptions(state), dispatch); + getTopologies(activeTopologyOptionsSelector(state), dispatch); getNodesDelta( getCurrentTopologyUrl(state), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), dispatch ); getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), state.get('nodeDetails'), dispatch ); @@ -269,7 +278,7 @@ export function clickNode(nodeId, label, origin) { getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), state.get('nodeDetails'), dispatch ); @@ -296,7 +305,7 @@ export function clickRelative(nodeId, topologyId, label, origin) { getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), state.get('nodeDetails'), dispatch ); @@ -325,7 +334,7 @@ export function clickShowTopologyForNode(topologyId, nodeId) { const state = getState(); getNodesDelta( getCurrentTopologyUrl(state), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), dispatch ); }; @@ -343,7 +352,7 @@ export function clickTopology(topologyId) { const state = getState(); getNodesDelta( getCurrentTopologyUrl(state), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), dispatch ); }; @@ -562,13 +571,13 @@ export function receiveTopologies(topologies) { const state = getState(); getNodesDelta( getCurrentTopologyUrl(state), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), dispatch ); getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), state.get('nodeDetails'), dispatch ); @@ -678,16 +687,16 @@ export function route(urlState) { }); // update all request workers with new options const state = getState(); - getTopologies(getActiveTopologyOptions(state), dispatch); + getTopologies(activeTopologyOptionsSelector(state), dispatch); getNodesDelta( getCurrentTopologyUrl(state), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), dispatch ); getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), state.get('nodeDetails'), dispatch ); @@ -720,7 +729,7 @@ export function changeInstance() { const state = getState(); getNodesDelta( getCurrentTopologyUrl(state), - getActiveTopologyOptions(state), + activeTopologyOptionsSelector(state), dispatch, true // forces websocket teardown and reconnect to new instance ); diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 5b8e7a062..14e80aa37 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -10,7 +10,7 @@ import Logo from '../components/logo'; import NodesChartElements from './nodes-chart-elements'; import { clickBackground, cacheZoomState } from '../actions/app-actions'; import { activeLayoutZoomSelector } from '../selectors/nodes-chart-zoom'; -import { activeTopologyZoomCacheKeyPath } from '../utils/topology-utils'; +import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/topology'; import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer'; @@ -146,7 +146,7 @@ class NodesChart extends React.Component { function mapStateToProps(state) { return { layoutZoom: activeLayoutZoomSelector(state), - layoutId: JSON.stringify(activeTopologyZoomCacheKeyPath(state)), + layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)), selectedNodeId: state.get('selectedNodeId'), forceRelayout: state.get('forceRelayout'), }; diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index f9af5671d..43970c0ef 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -21,7 +21,7 @@ import MetricSelector from './metric-selector'; import NetworkSelector from './networks-selector'; import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar'; import { getRouter, getUrlState } from '../utils/router-utils'; -import { getActiveTopologyOptions } from '../utils/topology-utils'; +import { activeTopologyOptionsSelector } from '../selectors/topology'; import { availableNetworksSelector } from '../selectors/node-networks'; const BACKSPACE_KEY_CODE = 8; @@ -143,7 +143,7 @@ class App extends React.Component { function mapStateToProps(state) { return { - activeTopologyOptions: getActiveTopologyOptions(state), + activeTopologyOptions: activeTopologyOptionsSelector(state), gridMode: state.get('gridMode'), routeSet: state.get('routeSet'), searchFocused: state.get('searchFocused'), diff --git a/client/app/scripts/components/topology-options.js b/client/app/scripts/components/topology-options.js index 89bf4ccc9..2c572060a 100644 --- a/client/app/scripts/components/topology-options.js +++ b/client/app/scripts/components/topology-options.js @@ -1,7 +1,8 @@ import React from 'react'; import { connect } from 'react-redux'; -import { getActiveTopologyOptions, getCurrentTopologyOptions } from '../utils/topology-utils'; +import { getCurrentTopologyOptions } from '../utils/topology-utils'; +import { activeTopologyOptionsSelector } from '../selectors/topology'; import TopologyOptionAction from './topology-option-action'; class TopologyOptions extends React.Component { @@ -37,7 +38,7 @@ function mapStateToProps(state) { return { options: getCurrentTopologyOptions(state), topologyId: state.get('currentTopologyId'), - activeOptions: getActiveTopologyOptions(state) + activeOptions: activeTopologyOptionsSelector(state) }; } diff --git a/client/app/scripts/reducers/__tests__/root-test.js b/client/app/scripts/reducers/__tests__/root-test.js index 6813f1700..f25701e0f 100644 --- a/client/app/scripts/reducers/__tests__/root-test.js +++ b/client/app/scripts/reducers/__tests__/root-test.js @@ -6,8 +6,9 @@ describe('RootReducer', () => { const reducer = require('../root').default; const initialState = require('../root').initialState; const topologyUtils = require('../../utils/topology-utils'); + const topologySelectors = require('../../selectors/topology'); // TODO maybe extract those to topology-utils tests? - const getActiveTopologyOptions = topologyUtils.getActiveTopologyOptions; + const activeTopologyOptionsSelector = topologySelectors.activeTopologyOptionsSelector; const getAdjacentNodes = topologyUtils.getAdjacentNodes; const isTopologyEmpty = topologyUtils.isTopologyEmpty; const getUrlState = require('../../utils/router-utils').getUrlState; @@ -241,28 +242,28 @@ describe('RootReducer', () => { nextState = reducer(nextState, ClickTopologyAction); // default options - expect(getActiveTopologyOptions(nextState).has('option1')).toBeTruthy(); - expect(getActiveTopologyOptions(nextState).get('option1')).toBe('off'); + expect(activeTopologyOptionsSelector(nextState).has('option1')).toBeTruthy(); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('off'); expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off'); // turn on nextState = reducer(nextState, ChangeTopologyOptionAction); - expect(getActiveTopologyOptions(nextState).get('option1')).toBe('on'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('on'); expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('on'); // turn off nextState = reducer(nextState, ChangeTopologyOptionAction2); - expect(getActiveTopologyOptions(nextState).get('option1')).toBe('off'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('off'); expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off'); // sub-topology should retain main topo options nextState = reducer(nextState, ClickSubTopologyAction); - expect(getActiveTopologyOptions(nextState).get('option1')).toBe('off'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('off'); expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off'); // other topology w/o options dont return options, but keep in app state nextState = reducer(nextState, ClickTopology2Action); - expect(getActiveTopologyOptions(nextState)).toBeUndefined(); + expect(activeTopologyOptionsSelector(nextState)).toBeUndefined(); expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off'); }); @@ -274,13 +275,13 @@ describe('RootReducer', () => { let nextState = initialState; nextState = reducer(nextState, RouteAction); - expect(getActiveTopologyOptions(nextState).get('option1')).toBe('on'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('on'); expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('on'); // stay same after topos have been received nextState = reducer(nextState, ReceiveTopologiesAction); nextState = reducer(nextState, ClickTopologyAction); - expect(getActiveTopologyOptions(nextState).get('option1')).toBe('on'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('on'); expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('on'); }); @@ -293,7 +294,7 @@ describe('RootReducer', () => { nextState = reducer(nextState, RouteAction); nextState = reducer(nextState, ReceiveTopologiesAction); nextState = reducer(nextState, ClickTopologyAction); - expect(getActiveTopologyOptions(nextState).get('option1')).toBe('off'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('off'); expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off'); }); diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index c38d8594d..bc1ad432f 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -6,7 +6,10 @@ import { fromJS, is as isDeepEqual, List as makeList, Map as makeMap, import ActionTypes from '../constants/action-types'; import { EDGE_ID_SEPARATOR } from '../constants/naming'; -import { graphExceedsComplexityThreshSelector } from '../selectors/topology'; +import { + graphExceedsComplexityThreshSelector, + activeTopologyZoomCacheKeyPathSelector, +} from '../selectors/topology'; import { applyPinnedSearches } from '../utils/search-utils'; import { getNetworkNodes } from '../utils/network-view-utils'; import { @@ -17,7 +20,6 @@ import { filterHiddenTopologies, addTopologyFullname, getDefaultTopology, - activeTopologyZoomCacheKeyPath, } from '../utils/topology-utils'; const log = debug('scope:app-store'); @@ -210,7 +212,7 @@ export function rootReducer(state = initialState, action) { } case ActionTypes.CACHE_ZOOM_STATE: { - return state.setIn(activeTopologyZoomCacheKeyPath(state), action.zoomState); + return state.setIn(activeTopologyZoomCacheKeyPathSelector(state), action.zoomState); } case ActionTypes.CLEAR_CONTROL_ERROR: { @@ -239,7 +241,7 @@ export function rootReducer(state = initialState, action) { case ActionTypes.CLICK_FORCE_RELAYOUT: { if (action.forceRelayout) { // Reset the zoom cache when forcing relayout. - state = state.deleteIn(activeTopologyZoomCacheKeyPath(state)); + state = state.deleteIn(activeTopologyZoomCacheKeyPathSelector(state)); } return state.set('forceRelayout', action.forceRelayout); } diff --git a/client/app/scripts/selectors/nodes-chart-graph.js b/client/app/scripts/selectors/nodes-chart-graph.js index a301e4997..39dbdb9c7 100644 --- a/client/app/scripts/selectors/nodes-chart-graph.js +++ b/client/app/scripts/selectors/nodes-chart-graph.js @@ -3,9 +3,9 @@ import { createSelector, createStructuredSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; import timely from 'timely'; -import { getActiveTopologyOptions } from '../utils/topology-utils'; import { initEdgesFromNodes, collapseMultiEdges } from '../utils/layouter-utils'; import { viewportWidthSelector, viewportHeightSelector } from './canvas-viewport'; +import { activeTopologyOptionsSelector } from './topology'; import { shownNodesSelector } from './node-filters'; import { doLayout } from '../charts/nodes-layout'; @@ -15,7 +15,7 @@ const log = debug('scope:nodes-chart'); const layoutOptionsSelector = createStructuredSelector({ forceRelayout: state => state.get('forceRelayout'), topologyId: state => state.get('currentTopologyId'), - topologyOptions: getActiveTopologyOptions, + topologyOptions: activeTopologyOptionsSelector, height: viewportHeightSelector, width: viewportWidthSelector, }); diff --git a/client/app/scripts/selectors/nodes-chart-layout.js b/client/app/scripts/selectors/nodes-chart-layout.js index f08e6de5b..a908d2ba0 100644 --- a/client/app/scripts/selectors/nodes-chart-layout.js +++ b/client/app/scripts/selectors/nodes-chart-layout.js @@ -52,7 +52,7 @@ const focusedNodesIdsSelector = createSelector( ], (selectedNodeId, nodes) => { if (!selectedNodeId || nodes.isEmpty()) { - return makeList(); + return []; } // The selected node always goes in focus. diff --git a/client/app/scripts/selectors/nodes-chart-zoom.js b/client/app/scripts/selectors/nodes-chart-zoom.js index 7c93e6792..b80e0cf6b 100644 --- a/client/app/scripts/selectors/nodes-chart-zoom.js +++ b/client/app/scripts/selectors/nodes-chart-zoom.js @@ -2,7 +2,7 @@ import { createSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; import { CANVAS_MARGINS, NODE_BASE_SIZE } from '../constants/styles'; -import { activeTopologyZoomCacheKeyPath } from '../utils/topology-utils'; +import { activeTopologyZoomCacheKeyPathSelector } from './topology'; import { viewportWidthSelector, viewportHeightSelector } from './canvas-viewport'; import { graphNodesSelector } from './nodes-chart-graph'; @@ -48,7 +48,7 @@ const defaultZoomSelector = createSelector( const activeLayoutCachedZoomSelector = createSelector( [ state => state.get('zoomCache'), - activeTopologyZoomCacheKeyPath, + activeTopologyZoomCacheKeyPathSelector, ], (zoomCache, keyPath) => zoomCache.getIn(keyPath.slice(1)) ); diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index c78017b5c..55dae58eb 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -11,3 +11,23 @@ export const graphExceedsComplexityThreshSelector = createSelector( ], (nodeCount, edgeCount) => (nodeCount + (2 * edgeCount)) > 1000 ); + +// Options for current topology, sub-topologies share options with parent +export const activeTopologyOptionsSelector = createSelector( + [ + state => state.getIn(['currentTopology', 'parentId']), + state => state.get('currentTopologyId'), + state => state.get('topologyOptions'), + ], + (parentTopologyId, currentTopologyId, topologyOptions) => ( + topologyOptions.get(parentTopologyId || currentTopologyId) + ) +); + +export const activeTopologyZoomCacheKeyPathSelector = createSelector( + [ + state => state.get('currentTopologyId'), + activeTopologyOptionsSelector, + ], + (topologyId, topologyOptions) => ['zoomCache', topologyId, JSON.stringify(topologyOptions)] +); diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js index ba820a615..2d3d2564f 100644 --- a/client/app/scripts/utils/topology-utils.js +++ b/client/app/scripts/utils/topology-utils.js @@ -127,15 +127,6 @@ export function filterHiddenTopologies(topologies) { t.stats.filtered_nodes > 0)); } -export function getActiveTopologyOptions(state) { - // options for current topology, sub-topologies share options with parent - const parentId = state.getIn(['currentTopology', 'parentId']); - if (parentId) { - return state.getIn(['topologyOptions', parentId]); - } - return state.getIn(['topologyOptions', state.get('currentTopologyId')]); -} - export function getCurrentTopologyOptions(state) { return state.getIn(['currentTopology', 'options']); } @@ -173,9 +164,3 @@ export function hasSelectedNode(state) { export function getCurrentTopologyUrl(state) { return state.getIn(['currentTopology', 'url']); } - -export function activeTopologyZoomCacheKeyPath(state) { - const topologyId = state.get('currentTopologyId'); - const topologyOptions = JSON.stringify(getActiveTopologyOptions(state)); - return ['zoomCache', topologyId, topologyOptions]; -}