diff --git a/client/app/scripts/charts/edge-container.js b/client/app/scripts/charts/edge-container.js
index d33241b0c..ac1db00a0 100644
--- a/client/app/scripts/charts/edge-container.js
+++ b/client/app/scripts/charts/edge-container.js
@@ -37,7 +37,7 @@ const waypointsMapToArray = (waypointsMap) => {
};
-class EdgeContainer extends React.Component {
+class EdgeContainer extends React.PureComponent {
constructor(props, context) {
super(props, context);
this.state = { waypointsMap: makeMap() };
diff --git a/client/app/scripts/charts/node-container.js b/client/app/scripts/charts/node-container.js
index 8f5817026..ca4ba8d65 100644
--- a/client/app/scripts/charts/node-container.js
+++ b/client/app/scripts/charts/node-container.js
@@ -7,35 +7,36 @@ import { NODES_SPRING_ANIMATION_CONFIG } from '../constants/animation';
import { NODE_BLUR_OPACITY } from '../constants/styles';
import Node from './node';
-const transformedNode = (otherProps, { x, y, k }) => (
-
+const transformedNode = (otherProps, { x, y, k, opacity }) => (
+ // NOTE: Controlling blurring and transform from here seems to re-render
+ // faster than adding a CSS class and controlling it from there.
+
+
+
);
-class NodeContainer extends React.Component {
+class NodeContainer extends React.PureComponent {
render() {
const { dx, dy, isAnimated, scale, blurred } = this.props;
const forwardedProps = omit(this.props, 'dx', 'dy', 'isAnimated', 'scale', 'blurred');
const opacity = blurred ? NODE_BLUR_OPACITY : 1;
- // NOTE: Controlling blurring from here seems to re-render faster
- // than adding a CSS class and controlling it from there.
+ if (!isAnimated) {
+ // Show static node for optimized rendering
+ return transformedNode(forwardedProps, { x: dx, y: dy, k: scale, opacity });
+ }
+
return (
-
- {!isAnimated ?
-
- // Show static node for optimized rendering
- transformedNode(forwardedProps, { x: dx, y: dy, k: scale }) :
-
- // Animate the node if the layout is sufficiently small
-
- {interpolated => transformedNode(forwardedProps, interpolated)}
- }
-
+ // Animate the node if the layout is sufficiently small
+
+ {interpolated => transformedNode(forwardedProps, interpolated)}
+
);
}
}
diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js
index cc8e52f50..8e034c734 100644
--- a/client/app/scripts/charts/node-shape-circle.js
+++ b/client/app/scripts/charts/node-shape-circle.js
@@ -14,7 +14,7 @@ import {
} from '../constants/styles';
-export default function NodeShapeCircle({id, highlighted, color, metric}) {
+export default function NodeShapeCircle({ id, highlighted, color, metric }) {
const { height, hasMetric, formattedValue } = getMetricValue(metric);
const metricStyle = { fill: getMetricColor(metric) };
diff --git a/client/app/scripts/charts/node-shape-cloud.js b/client/app/scripts/charts/node-shape-cloud.js
index 3278ea651..0b2efdffb 100644
--- a/client/app/scripts/charts/node-shape-cloud.js
+++ b/client/app/scripts/charts/node-shape-cloud.js
@@ -14,7 +14,7 @@ const CLOUD_PATH = 'M-125 23.333Q-125 44.036-110.352 58.685-95.703 73.333-75 73.
+ '11.458-16.732 11.458-24.414 29.948-9.115-8.073-21.614-8.073-13.802 0-23.568 9.766-9.766 9.766-'
+ '9.766 23.568 0 9.766 5.339 17.968-16.797 3.906-27.735 17.513-10.938 13.607-10.937 31.185z';
-export default function NodeShapeCloud({highlighted, color}) {
+export default function NodeShapeCloud({ highlighted, color }) {
const pathProps = r => ({ d: CLOUD_PATH, transform: `scale(${r / NODE_BASE_SIZE})` });
return (
diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js
index 9cd392749..7eec557da 100644
--- a/client/app/scripts/charts/node.js
+++ b/client/app/scripts/charts/node.js
@@ -41,12 +41,11 @@ function getNodeShape({ shape, stack }) {
}
-class Node extends React.Component {
+class Node extends React.PureComponent {
constructor(props, context) {
super(props, context);
this.state = {
hovered: false,
- matched: false
};
this.handleMouseClick = this.handleMouseClick.bind(this);
@@ -55,15 +54,6 @@ class Node extends React.Component {
this.saveShapeRef = this.saveShapeRef.bind(this);
}
- componentWillReceiveProps(nextProps) {
- // marks as matched only when search query changes
- if (nextProps.searchQuery !== this.props.searchQuery) {
- this.setState({ matched: nextProps.matched });
- } else {
- this.setState({ matched: false });
- }
- }
-
renderSvgLabels(labelClassName, subLabelClassName, labelOffsetY) {
const { label, subLabel } = this.props;
return (
@@ -77,7 +67,7 @@ class Node extends React.Component {
}
renderStandardLabels(labelClassName, subLabelClassName, labelOffsetY, mouseEvents) {
- const { label, subLabel, blurred, matches = makeMap() } = this.props;
+ const { label, subLabel, matches = makeMap() } = this.props;
const matchedMetadata = matches.get('metadata', makeList());
const matchedParents = matches.get('parents', makeList());
const matchedNodeDetails = matchedMetadata.concat(matchedParents);
@@ -96,16 +86,16 @@ class Node extends React.Component {
- {!blurred && }
+
);
}
render() {
- const { blurred, focused, highlighted, networks, pseudo, rank, label,
- transform, exportingGraph, showingNetworks, stack } = this.props;
- const { hovered, matched } = this.state;
+ const { focused, highlighted, networks, pseudo, rank, label,
+ transform, exportingGraph, showingNetworks, stack, id, metric } = this.props;
+ const { hovered } = this.state;
const color = getNodeColor(rank, label, pseudo);
const truncate = !focused && !hovered;
@@ -113,9 +103,7 @@ class Node extends React.Component {
const nodeClassName = classnames('node', {
highlighted,
- blurred: blurred && !focused,
hovered,
- matched,
pseudo
});
@@ -123,7 +111,6 @@ class Node extends React.Component {
const subLabelClassName = classnames('node-sublabel', { truncate });
const NodeShapeType = getNodeShape(this.props);
- const useSvgLabels = exportingGraph;
const mouseEvents = {
onClick: this.handleMouseClick,
onMouseEnter: this.handleMouseEnter,
@@ -132,12 +119,12 @@ class Node extends React.Component {
return (
- {useSvgLabels ?
+ {exportingGraph ?
this.renderSvgLabels(labelClassName, subLabelClassName, labelOffsetY) :
this.renderStandardLabels(labelClassName, subLabelClassName, labelOffsetY, mouseEvents)}
-
+
{showingNetworks && }
@@ -167,7 +154,6 @@ class Node extends React.Component {
export default connect(
state => ({
- searchQuery: state.get('searchQuery'),
exportingGraph: state.get('exportingGraph'),
showingNetworks: state.get('showingNetworks'),
}),
diff --git a/client/app/scripts/charts/nodes-chart-edges.js b/client/app/scripts/charts/nodes-chart-edges.js
index 402cf0b1b..245edfac1 100644
--- a/client/app/scripts/charts/nodes-chart-edges.js
+++ b/client/app/scripts/charts/nodes-chart-edges.js
@@ -1,7 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
-import { Map as makeMap, List as makeList } from 'immutable';
+import { List as makeList } from 'immutable';
+import { currentTopologySearchNodeMatchesSelector } from '../selectors/search';
import { hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils';
import EdgeContainer from './edge-container';
@@ -9,7 +10,7 @@ class NodesChartEdges extends React.Component {
render() {
const { hasSelectedNode, highlightedEdgeIds, layoutEdges, searchQuery,
isAnimated, selectedScale, selectedNodeId, selectedNetwork, selectedNetworkNodes,
- searchNodeMatches = makeMap() } = this.props;
+ searchNodeMatches } = this.props;
return (
@@ -49,19 +50,14 @@ class NodesChartEdges extends React.Component {
}
}
-function mapStateToProps(state) {
- const currentTopologyId = state.get('currentTopologyId');
- return {
+export default connect(
+ state => ({
hasSelectedNode: hasSelectedNodeFn(state),
highlightedEdgeIds: state.get('highlightedEdgeIds'),
- searchNodeMatches: state.getIn(['searchNodeMatches', currentTopologyId]),
+ searchNodeMatches: currentTopologySearchNodeMatchesSelector(state),
searchQuery: state.get('searchQuery'),
selectedNetwork: state.get('selectedNetwork'),
selectedNetworkNodes: state.getIn(['networkNodes', state.get('selectedNetwork')], makeList()),
selectedNodeId: state.get('selectedNodeId'),
- };
-}
-
-export default connect(
- mapStateToProps
+ })
)(NodesChartEdges);
diff --git a/client/app/scripts/charts/nodes-chart-elements.js b/client/app/scripts/charts/nodes-chart-elements.js
index dc16e0b73..728ce4448 100644
--- a/client/app/scripts/charts/nodes-chart-elements.js
+++ b/client/app/scripts/charts/nodes-chart-elements.js
@@ -1,7 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
-import { completeNodesSelector } from '../selectors/chartSelectors';
import NodesChartEdges from './nodes-chart-edges';
import NodesChartNodes from './nodes-chart-nodes';
@@ -15,7 +14,7 @@ class NodesChartElements extends React.Component {
selectedScale={props.selectedScale}
isAnimated={props.isAnimated} />
@@ -23,10 +22,4 @@ class NodesChartElements extends React.Component {
}
}
-function mapStateToProps(state, props) {
- return {
- completeNodes: completeNodesSelector(state, props)
- };
-}
-
-export default connect(mapStateToProps)(NodesChartElements);
+export default connect()(NodesChartElements);
diff --git a/client/app/scripts/charts/nodes-chart-nodes.js b/client/app/scripts/charts/nodes-chart-nodes.js
index 9ce3c1181..e04da2677 100644
--- a/client/app/scripts/charts/nodes-chart-nodes.js
+++ b/client/app/scripts/charts/nodes-chart-nodes.js
@@ -1,7 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
-import { fromJS, Map as makeMap, List as makeList } from 'immutable';
+import { fromJS, List as makeList } from 'immutable';
+import { currentTopologySearchNodeMatchesSelector } from '../selectors/search';
import { getAdjacentNodes } from '../utils/topology-utils';
import NodeContainer from './node-container';
@@ -9,7 +10,7 @@ class NodesChartNodes extends React.Component {
render() {
const { adjacentNodes, highlightedNodeIds, layoutNodes, isAnimated, mouseOverNodeId,
selectedScale, searchQuery, selectedMetric, selectedNetwork, selectedNodeId,
- topCardNode, searchNodeMatches = makeMap() } = this.props;
+ topCardNode, searchNodeMatches } = this.props;
// highlighter functions
const setHighlighted = node => node.set('highlighted',
@@ -56,7 +57,6 @@ class NodesChartNodes extends React.Component {
{nodesToRender.map(node => ({
adjacentNodes: getAdjacentNodes(state),
highlightedNodeIds: state.get('highlightedNodeIds'),
mouseOverNodeId: state.get('mouseOverNodeId'),
selectedMetric: state.get('selectedMetric'),
selectedNetwork: state.get('selectedNetwork'),
selectedNodeId: state.get('selectedNodeId'),
- searchNodeMatches: state.getIn(['searchNodeMatches', currentTopologyId]),
+ searchNodeMatches: currentTopologySearchNodeMatchesSelector(state),
searchQuery: state.get('searchQuery'),
topCardNode: state.get('nodeDetails').last()
- };
-}
-
-export default connect(
- mapStateToProps
+ })
)(NodesChartNodes);
diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js
index 33a40fd73..080e52828 100644
--- a/client/app/scripts/charts/nodes-chart.js
+++ b/client/app/scripts/charts/nodes-chart.js
@@ -6,7 +6,7 @@ import { Map as makeMap } from 'immutable';
import { event as d3Event, select } from 'd3-selection';
import { zoom, zoomIdentity } from 'd3-zoom';
-import { nodeAdjacenciesSelector, adjacentNodesSelector } from '../selectors/chartSelectors';
+import { nodeAdjacenciesSelector } from '../selectors/nodes-chart';
import { clickBackground } from '../actions/app-actions';
import Logo from '../components/logo';
import NodesChartElements from './nodes-chart-elements';
@@ -17,7 +17,7 @@ import { layoutWithSelectedNode } from '../selectors/nodes-chart-focus';
import { graphLayout } from '../selectors/nodes-chart-layout';
-const GRAPH_COMPLEXITY_NODES_TRESHOLD = 100;
+const GRAPH_COMPLEXITY_NODES_TRESHOLD = 200;
const ZOOM_CACHE_FIELDS = [
'panTranslateX', 'panTranslateY',
'zoomScale', 'minZoomScale', 'maxZoomScale'
@@ -173,7 +173,6 @@ class NodesChart extends React.Component {
function mapStateToProps(state) {
return {
nodes: nodeAdjacenciesSelector(state),
- adjacentNodes: adjacentNodesSelector(state),
forceRelayout: state.get('forceRelayout'),
selectedNodeId: state.get('selectedNodeId'),
topologyId: state.get('currentTopologyId'),
diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js
index d3de7f043..07831fa60 100644
--- a/client/app/scripts/charts/nodes-grid.js
+++ b/client/app/scripts/charts/nodes-grid.js
@@ -5,8 +5,9 @@ import { connect } from 'react-redux';
import { List as makeList, Map as makeMap } from 'immutable';
import NodeDetailsTable from '../components/node-details/node-details-table';
import { clickNode, sortOrderChanged } from '../actions/app-actions';
-import { nodesSelector } from '../selectors/chartSelectors';
+import { nodesSelector } from '../selectors/nodes-chart';
+import { currentTopologySearchNodeMatchesSelector } from '../selectors/search';
import { getNodeColor } from '../utils/color-utils';
@@ -96,7 +97,7 @@ class NodesGrid extends React.Component {
render() {
const { margins, nodes, height, gridSortedBy, gridSortedDesc,
- searchNodeMatches = makeMap(), searchQuery } = this.props;
+ searchNodeMatches, searchQuery } = this.props;
const cmpStyle = {
height,
marginTop: margins.top,
@@ -148,7 +149,8 @@ function mapStateToProps(state) {
gridSortedDesc: state.get('gridSortedDesc'),
currentTopology: state.get('currentTopology'),
currentTopologyId: state.get('currentTopologyId'),
- searchNodeMatches: state.getIn(['searchNodeMatches', state.get('currentTopologyId')]),
+ searchNodeMatches: currentTopologySearchNodeMatchesSelector(state),
+ // searchNodeMatches: state.getIn(['searchNodeMatches', state.get('currentTopologyId')]),
searchQuery: state.get('searchQuery'),
selectedNodeId: state.get('selectedNodeId')
};
diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js
index 635fb8092..7a2e42bc9 100644
--- a/client/app/scripts/charts/nodes-layout.js
+++ b/client/app/scripts/charts/nodes-layout.js
@@ -10,12 +10,10 @@ import { buildTopologyCacheId, updateNodeDegrees } from '../utils/topology-utils
const log = debug('scope:nodes-layout');
const topologyCaches = {};
-export const DEFAULT_WIDTH = 800;
-export const DEFAULT_HEIGHT = DEFAULT_WIDTH / 2;
export const DEFAULT_MARGINS = {top: 0, left: 0};
const NODE_SIZE_FACTOR = NODE_BASE_SIZE;
-const NODE_SEPARATION_FACTOR = 2 * NODE_BASE_SIZE;
-const RANK_SEPARATION_FACTOR = 3 * NODE_BASE_SIZE;
+const NODE_SEPARATION_FACTOR = 1.5 * NODE_BASE_SIZE;
+const RANK_SEPARATION_FACTOR = 2.5 * NODE_BASE_SIZE;
let layoutRuns = 0;
let layoutRunsTrivial = 0;
@@ -89,7 +87,7 @@ function runLayoutEngine(graph, imNodes, imEdges) {
}
});
- dagre.layout(graph);
+ dagre.layout(graph, { debugTiming: false });
const layout = graph.graph();
// apply coordinates to nodes and edges
diff --git a/client/app/scripts/components/help-panel.js b/client/app/scripts/components/help-panel.js
index ef442a1b4..d672ed420 100644
--- a/client/app/scripts/components/help-panel.js
+++ b/client/app/scripts/components/help-panel.js
@@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
-import { searchableFieldsSelector } from '../selectors/chartSelectors';
+import { searchableFieldsSelector } from '../selectors/search';
import { CANVAS_MARGINS } from '../constants/styles';
import { hideHelp } from '../actions/app-actions';
diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js
index e0ef693e4..d57ab02ba 100644
--- a/client/app/scripts/components/search.js
+++ b/client/app/scripts/components/search.js
@@ -4,6 +4,7 @@ import classnames from 'classnames';
import { debounce } from 'lodash';
import { blurSearch, doSearch, focusSearch, showHelp } from '../actions/app-actions';
+import { searchNodeMatchesSelector } from '../selectors/search';
import { slugify } from '../utils/string-utils';
import { isTopologyEmpty } from '../utils/topology-utils';
import SearchItem from './search-item';
@@ -156,7 +157,7 @@ export default connect(
pinnedSearches: state.get('pinnedSearches'),
searchFocused: state.get('searchFocused'),
searchQuery: state.get('searchQuery'),
- searchNodeMatches: state.get('searchNodeMatches'),
+ searchNodeMatches: searchNodeMatchesSelector(state),
topologiesLoaded: state.get('topologiesLoaded')
}),
{ blurSearch, doSearch, focusSearch, onClickHelp: showHelp }
diff --git a/client/app/scripts/components/topologies.js b/client/app/scripts/components/topologies.js
index df180d319..272799553 100644
--- a/client/app/scripts/components/topologies.js
+++ b/client/app/scripts/components/topologies.js
@@ -2,6 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
+import { searchNodeMatchesSelector } from '../selectors/search';
import { clickTopology } from '../actions/app-actions';
@@ -90,7 +91,7 @@ class Topologies extends React.Component {
function mapStateToProps(state) {
return {
topologies: state.get('topologies'),
- searchNodeMatches: state.get('searchNodeMatches'),
+ searchNodeMatches: searchNodeMatchesSelector(state),
currentTopology: state.get('currentTopology')
};
}
diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js
index 20f2a70af..2474ba97e 100644
--- a/client/app/scripts/reducers/root.js
+++ b/client/app/scripts/reducers/root.js
@@ -5,7 +5,7 @@ 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 { applyPinnedSearches, updateNodeMatches } from '../utils/search-utils';
+import { applyPinnedSearches } from '../utils/search-utils';
import { getNetworkNodes, getAvailableNetworks } from '../utils/network-view-utils';
import {
findTopologyById,
@@ -59,7 +59,6 @@ export const initialState = makeMap({
pinnedSearches: makeList(), // list of node filters
routeSet: false,
searchFocused: false,
- searchNodeMatches: makeMap(),
searchQuery: null,
selectedMetric: null,
selectedNetwork: null,
@@ -386,8 +385,7 @@ export function rootReducer(state = initialState, action) {
}
case ActionTypes.DO_SEARCH: {
- state = state.set('searchQuery', action.searchQuery);
- return updateNodeMatches(state);
+ return state.set('searchQuery', action.searchQuery);
}
case ActionTypes.ENTER_EDGE: {
@@ -473,10 +471,9 @@ export function rootReducer(state = initialState, action) {
}
case ActionTypes.PIN_SEARCH: {
- state = state.set('searchQuery', '');
- state = updateNodeMatches(state);
const pinnedSearches = state.get('pinnedSearches');
state = state.setIn(['pinnedSearches', pinnedSearches.size], action.query);
+ state = state.set('searchQuery', '');
return applyPinnedSearches(state);
}
@@ -613,18 +610,12 @@ export function rootReducer(state = initialState, action) {
state = state.set('selectedMetric', state.get('pinnedMetric'));
}
- // update nodes cache and search results
- state = state.setIn(['nodesByTopology', state.get('currentTopologyId')], state.get('nodes'));
- state = updateNodeMatches(state);
-
- return state;
+ // update nodes cache
+ return state.setIn(['nodesByTopology', state.get('currentTopologyId')], state.get('nodes'));
}
case ActionTypes.RECEIVE_NODES_FOR_TOPOLOGY: {
- // not sure if mergeDeep() brings any benefit here
- state = state.setIn(['nodesByTopology', action.topologyId], fromJS(action.nodes));
- state = updateNodeMatches(state);
- return state;
+ return state.setIn(['nodesByTopology', action.topologyId], fromJS(action.nodes));
}
case ActionTypes.RECEIVE_NOT_FOUND: {
diff --git a/client/app/scripts/selectors/chartSelectors.js b/client/app/scripts/selectors/chartSelectors.js
deleted file mode 100644
index 223a4d30a..000000000
--- a/client/app/scripts/selectors/chartSelectors.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import debug from 'debug';
-import { identity } from 'lodash';
-import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
-import { Map as makeMap, is, Set } from 'immutable';
-
-import { getAdjacentNodes } from '../utils/topology-utils';
-import { getSearchableFields } from '../utils/search-utils';
-
-
-const log = debug('scope:selectors');
-
-
-//
-// `mergeDeepKeyIntersection` does a deep merge on keys that exists in both maps
-//
-function mergeDeepKeyIntersection(mapA, mapB) {
- const commonKeys = Set.fromKeys(mapA).intersect(mapB.keySeq());
- return makeMap(commonKeys.map(k => [k, mapA.get(k).mergeDeep(mapB.get(k))]));
-}
-
-
-//
-// `returnPreviousRefIfEqual` is a helper function that checks the new computed of a selector
-// against the previously computed value. If they are deeply equal return the previous result. This
-// is important for things like connect() which tests whether componentWillReceiveProps should be
-// called by doing a '===' on the values you return from mapStateToProps.
-//
-// e.g.
-//
-// const filteredThings = createSelector(
-// state => state.things,
-// (things) => things.filter(t => t > 2)
-// );
-//
-// // This will trigger componentWillReceiveProps on every store change:
-// connect(s => { things: filteredThings(s) }, ThingComponent);
-//
-// // But if we wrap it, the result will be === if it `is()` equal and...
-// const filteredThingsWrapped = returnPreviousRefIfEqual(filteredThings);
-//
-// // ...We're safe!
-// connect(s => { things: filteredThingsWrapped(s) }, ThingComponent);
-//
-// Note: This is a slightly strange way to use reselect. Selectors memoize their *arguments* not
-// "their results", so use the result of the wrapped selector as the argument to another selector
-// here to memoize it and get what we want.
-//
-const createDeepEqualSelector = createSelectorCreator(defaultMemoize, is);
-const returnPreviousRefIfEqual = selector => createDeepEqualSelector(selector, identity);
-
-
-//
-// Selectors!
-//
-
-
-const allNodesSelector = state => state.get('nodes');
-
-
-export const nodesSelector = returnPreviousRefIfEqual(
- createSelector(
- allNodesSelector,
- allNodes => allNodes.filter(node => !node.get('filtered'))
- )
-);
-
-
-export const adjacentNodesSelector = returnPreviousRefIfEqual(getAdjacentNodes);
-
-
-export const nodeAdjacenciesSelector = returnPreviousRefIfEqual(
- createSelector(
- nodesSelector,
- nodes => nodes.map(n => makeMap({
- id: n.get('id'),
- adjacency: n.get('adjacency'),
- }))
- )
-);
-
-
-export const dataNodesSelector = createSelector(
- nodesSelector,
- nodes => nodes.map((node, id) => makeMap({
- id,
- label: node.get('label'),
- pseudo: node.get('pseudo'),
- subLabel: node.get('labelMinor'),
- nodeCount: node.get('node_count'),
- metrics: node.get('metrics'),
- rank: node.get('rank'),
- shape: node.get('shape'),
- stack: node.get('stack'),
- networks: node.get('networks'),
- }))
-);
-
-
-export const searchableFieldsSelector = returnPreviousRefIfEqual(
- createSelector(
- allNodesSelector,
- getSearchableFields
- )
-);
-
-
-//
-// FIXME: this is a bit of a hack...
-//
-export const layoutNodesSelector = (_, props) => props.layoutNodes || makeMap();
-
-
-export const completeNodesSelector = createSelector(
- layoutNodesSelector,
- dataNodesSelector,
- (layoutNodes, dataNodes) => {
- //
- // There are no guarantees whether this selector will be computed first (when
- // node-chart-elements.mapStateToProps is called by store.subscribe before
- // nodes-chart.mapStateToProps is called), and component render batching and yadada.
- //
- if (layoutNodes.size !== dataNodes.size) {
- log('Obviously mismatched node data', layoutNodes.size, dataNodes.size);
- }
- return mergeDeepKeyIntersection(dataNodes, layoutNodes);
- }
-);
diff --git a/client/app/scripts/selectors/nodes-chart-focus.js b/client/app/scripts/selectors/nodes-chart-focus.js
index 15ad07f0f..a5b4ce637 100644
--- a/client/app/scripts/selectors/nodes-chart-focus.js
+++ b/client/app/scripts/selectors/nodes-chart-focus.js
@@ -1,7 +1,7 @@
import { includes, without } from 'lodash';
-import { fromJS } from 'immutable';
import { createSelector } from 'reselect';
import { scaleThreshold } from 'd3-scale';
+import { fromJS, Set as makeSet } from 'immutable';
import { NODE_BASE_SIZE, DETAILS_PANEL_WIDTH } from '../constants/styles';
@@ -21,8 +21,8 @@ const stateHeightSelector = state => state.height;
const stateScaleSelector = state => state.zoomScale;
const stateTranslateXSelector = state => state.panTranslateX;
const stateTranslateYSelector = state => state.panTranslateY;
+const inputNodesSelector = (_, props) => props.nodes;
const propsSelectedNodeIdSelector = (_, props) => props.selectedNodeId;
-const propsAdjacentNodesSelector = (_, props) => props.adjacentNodes;
const propsMarginsSelector = (_, props) => props.margins;
// The narrower dimension of the viewport, used for scaling.
@@ -60,9 +60,26 @@ const viewportCenterSelector = createSelector(
const selectedNodeNeighborsIdsSelector = createSelector(
[
propsSelectedNodeIdSelector,
- propsAdjacentNodesSelector,
+ inputNodesSelector,
],
- (selectedNodeId, adjacentNodes) => without(adjacentNodes.toArray(), selectedNodeId)
+ (selectedNodeId, nodes) => {
+ let adjacentNodes = makeSet();
+ if (!selectedNodeId) {
+ return adjacentNodes;
+ }
+
+ if (nodes && nodes.has(selectedNodeId)) {
+ adjacentNodes = makeSet(nodes.getIn([selectedNodeId, 'adjacency']));
+ // fill up set with reverse edges
+ nodes.forEach((node, id) => {
+ if (node.get('adjacency') && node.get('adjacency').includes(selectedNodeId)) {
+ adjacentNodes = adjacentNodes.add(id);
+ }
+ });
+ }
+
+ return without(adjacentNodes.toArray(), selectedNodeId);
+ }
);
const selectedNodesLayoutSettingsSelector = createSelector(
diff --git a/client/app/scripts/selectors/nodes-chart-layout.js b/client/app/scripts/selectors/nodes-chart-layout.js
index e5c8a8e73..bb61cf8da 100644
--- a/client/app/scripts/selectors/nodes-chart-layout.js
+++ b/client/app/scripts/selectors/nodes-chart-layout.js
@@ -25,7 +25,7 @@ function initEdgesFromNodes(nodes) {
const adjacency = node.get('adjacency');
if (adjacency) {
adjacency.forEach((adjacent) => {
- const edge = [nodeId, adjacent];
+ const edge = nodeId < adjacent ? [nodeId, adjacent] : [adjacent, nodeId];
const edgeId = edge.join(EDGE_ID_SEPARATOR);
if (!edges.has(edgeId)) {
@@ -87,6 +87,16 @@ export const graphLayout = createSelector(
const layoutNodes = graph.nodes.map(node => makeMap({
x: node.get('x'),
y: node.get('y'),
+ id: node.get('id'),
+ label: node.get('label'),
+ pseudo: node.get('pseudo'),
+ subLabel: node.get('labelMinor'),
+ nodeCount: node.get('node_count'),
+ metrics: node.get('metrics'),
+ rank: node.get('rank'),
+ shape: node.get('shape'),
+ stack: node.get('stack'),
+ // networks: node.get('networks'),
}));
return { layoutNodes, layoutEdges };
diff --git a/client/app/scripts/selectors/nodes-chart.js b/client/app/scripts/selectors/nodes-chart.js
new file mode 100644
index 000000000..040fb5f26
--- /dev/null
+++ b/client/app/scripts/selectors/nodes-chart.js
@@ -0,0 +1,32 @@
+import { createSelector } from 'reselect';
+import { Map as makeMap } from 'immutable';
+
+
+const allNodesSelector = state => state.get('nodes');
+
+export const nodesSelector = createSelector(
+ [
+ allNodesSelector,
+ ],
+ allNodes => allNodes.filter(node => !node.get('filtered'))
+);
+
+
+export const nodeAdjacenciesSelector = createSelector(
+ [
+ nodesSelector,
+ ],
+ nodes => nodes.map(node => makeMap({
+ id: node.get('id'),
+ adjacency: node.get('adjacency'),
+ label: node.get('label'),
+ pseudo: node.get('pseudo'),
+ subLabel: node.get('labelMinor'),
+ nodeCount: node.get('node_count'),
+ metrics: node.get('metrics'),
+ rank: node.get('rank'),
+ shape: node.get('shape'),
+ stack: node.get('stack'),
+ networks: node.get('networks'),
+ }))
+);
diff --git a/client/app/scripts/selectors/search.js b/client/app/scripts/selectors/search.js
new file mode 100644
index 000000000..97738438c
--- /dev/null
+++ b/client/app/scripts/selectors/search.js
@@ -0,0 +1,41 @@
+import { createSelector } from 'reselect';
+import { createMapSelector } from 'reselect-map';
+import { Map as makeMap } from 'immutable';
+
+import { parseQuery, searchTopology, getSearchableFields } from '../utils/search-utils';
+
+
+const allNodesSelector = state => state.get('nodes');
+const nodesByTopologySelector = state => state.get('nodesByTopology');
+const currentTopologyIdSelector = state => state.get('currentTopologyId');
+const searchQuerySelector = state => state.get('searchQuery');
+
+const parsedSearchQuerySelector = createSelector(
+ [
+ searchQuerySelector
+ ],
+ searchQuery => parseQuery(searchQuery)
+);
+
+export const searchNodeMatchesSelector = createMapSelector(
+ [
+ nodesByTopologySelector,
+ parsedSearchQuerySelector,
+ ],
+ (nodes, parsed) => (parsed ? searchTopology(nodes, parsed) : makeMap())
+);
+
+export const currentTopologySearchNodeMatchesSelector = createSelector(
+ [
+ searchNodeMatchesSelector,
+ currentTopologyIdSelector,
+ ],
+ (nodesByTopology, currentTopologyId) => nodesByTopology.get(currentTopologyId) || makeMap()
+);
+
+export const searchableFieldsSelector = createSelector(
+ [
+ allNodesSelector,
+ ],
+ getSearchableFields
+);
diff --git a/client/app/scripts/utils/__tests__/search-utils-test.js b/client/app/scripts/utils/__tests__/search-utils-test.js
index dde59e679..d6745db6d 100644
--- a/client/app/scripts/utils/__tests__/search-utils-test.js
+++ b/client/app/scripts/utils/__tests__/search-utils-test.js
@@ -304,54 +304,4 @@ describe('SearchUtils', () => {
expect(matches4.get('row3_c').text).toBe('Value 3');
});
});
-
- describe('updateNodeMatches', () => {
- const fun = SearchUtils.updateNodeMatches;
-
- it('should return no matches on an empty topology', () => {
- let nextState = fromJS({
- nodesByTopology: {},
- searchNodeMatches: {},
- searchQuery: ''
- });
- nextState = fun(nextState);
- expect(nextState.get('searchNodeMatches').size).toEqual(0);
- });
-
- it('should return no matches when no query is present', () => {
- let nextState = fromJS({
- nodesByTopology: {topo1: nodeSets.someNodes},
- searchNodeMatches: {},
- searchQuery: ''
- });
- nextState = fun(nextState);
- expect(nextState.get('searchNodeMatches').size).toEqual(0);
- });
-
- it('should return no matches when query matches nothing', () => {
- let nextState = fromJS({
- nodesByTopology: {topo1: nodeSets.someNodes},
- searchNodeMatches: {},
- searchQuery: 'cantmatch'
- });
- nextState = fun(nextState);
- expect(nextState.get('searchNodeMatches').size).toEqual(0);
- });
-
- it('should return a matches when a query matches something', () => {
- let nextState = fromJS({
- nodesByTopology: {topo1: nodeSets.someNodes},
- searchNodeMatches: {},
- searchQuery: 'value 2'
- });
- nextState = fun(nextState);
- expect(nextState.get('searchNodeMatches').size).toEqual(1);
- expect(nextState.get('searchNodeMatches').get('topo1').size).toEqual(1);
-
- // then clear up again
- nextState = nextState.set('searchQuery', '');
- nextState = fun(nextState);
- expect(nextState.get('searchNodeMatches').size).toEqual(0);
- });
- });
});
diff --git a/client/app/scripts/utils/search-utils.js b/client/app/scripts/utils/search-utils.js
index 67064904f..761421944 100644
--- a/client/app/scripts/utils/search-utils.js
+++ b/client/app/scripts/utils/search-utils.js
@@ -232,29 +232,6 @@ export function parseQuery(query) {
return null;
}
-/**
- * Returns {topologyId: {nodeId: matches}}
- */
-export function updateNodeMatches(state) {
- const parsed = parseQuery(state.get('searchQuery'));
- if (parsed) {
- if (state.has('nodesByTopology')) {
- state.get('nodesByTopology').forEach((nodes, topologyId) => {
- const nodeMatches = searchTopology(nodes, parsed);
- if (nodeMatches.size > 0) {
- state = state.setIn(['searchNodeMatches', topologyId], nodeMatches);
- } else {
- state = state.deleteIn(['searchNodeMatches', topologyId]);
- }
- });
- }
- } else if (state.has('searchNodeMatches')) {
- state = state.update('searchNodeMatches', snm => snm.clear());
- }
-
- return state;
-}
-
export function getSearchableFields(nodes) {
const get = (node, key) => node.get(key) || makeList();
@@ -269,8 +246,9 @@ export function getSearchableFields(nodes) {
labels.union(get(node, 'parents').map(p => p.get('topologyId')))
), makeSet());
+ // Consider only property lists (and not generic tables).
const tableRowLabels = nodes.reduce((labels, node) => (
- labels.union(get(node, 'tables').flatMap(t => (t.get('rows') || makeList)
+ labels.union(get(node, 'tables').filter(isPropertyList).flatMap(t => (t.get('rows') || makeList)
.map(f => f.getIn(['entries', 'label']))
))
), makeSet());
@@ -325,5 +303,4 @@ export const testable = {
parseQuery,
parseValue,
searchTopology,
- updateNodeMatches
};
diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js
index 9a7485641..df4dab82a 100644
--- a/client/app/scripts/utils/topology-utils.js
+++ b/client/app/scripts/utils/topology-utils.js
@@ -180,7 +180,7 @@ export function isNodeMatchingQuery(node, query) {
export function graphExceedsComplexityThresh(stats) {
// Check to see if complexity is high. Used to trigger table view on page load.
- return (stats.get('node_count') + (2 * stats.get('edge_count'))) > 500;
+ return (stats.get('node_count') + (2 * stats.get('edge_count'))) > 1000;
}
export function zoomCacheKey(props) {
diff --git a/client/package.json b/client/package.json
index dff7cfaf1..4ce6c23b7 100644
--- a/client/package.json
+++ b/client/package.json
@@ -29,6 +29,7 @@
"moment": "2.17.1",
"page": "1.7.1",
"react": "15.4.1",
+ "react-addons-perf": "15.4.1",
"react-dom": "15.4.1",
"react-motion": "0.4.5",
"react-redux": "4.4.6",
@@ -39,6 +40,7 @@
"redux-thunk": "2.1.0",
"reqwest": "2.0.5",
"reselect": "2.5.4",
+ "reselect-map": "1.0.0",
"timely": "0.1.0",
"whatwg-fetch": "2.0.1",
"react-addons-perf": "15.4.1",