diff --git a/client/app/scripts/charts/nodes-chart-edges.js b/client/app/scripts/charts/nodes-chart-edges.js
index 3603c1341..760f03660 100644
--- a/client/app/scripts/charts/nodes-chart-edges.js
+++ b/client/app/scripts/charts/nodes-chart-edges.js
@@ -1,50 +1,77 @@
import React from 'react';
import { connect } from 'react-redux';
+import { Map as makeMap } from 'immutable';
+import { searchNodeMatchesSelector } from '../selectors/search';
import { selectedNetworkNodesIdsSelector } from '../selectors/node-networks';
-import { currentTopologySearchNodeMatchesSelector } from '../selectors/search';
import { hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils';
import EdgeContainer from './edge-container';
class NodesChartEdges extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+
+ // Edge decorators
+ this.edgeFocusedDecorator = this.edgeFocusedDecorator.bind(this);
+ this.edgeBlurredDecorator = this.edgeBlurredDecorator.bind(this);
+ this.edgeHighlightedDecorator = this.edgeHighlightedDecorator.bind(this);
+ this.edgeScaleDecorator = this.edgeScaleDecorator.bind(this);
+ }
+
+ edgeHighlightedDecorator(edge) {
+ return edge.set('highlighted', this.props.highlightedEdgeIds.has(edge.get('id')));
+ }
+
+ edgeFocusedDecorator(edge) {
+ const sourceSelected = (this.props.selectedNodeId === edge.get('source'));
+ const targetSelected = (this.props.selectedNodeId === edge.get('target'));
+ return edge.set('focused', this.props.hasSelectedNode && (sourceSelected || targetSelected));
+ }
+
+ edgeBlurredDecorator(edge) {
+ const { selectedNodeId, searchNodeMatches, selectedNetworkNodesIds } = this.props;
+ const sourceSelected = (selectedNodeId === edge.get('source'));
+ const targetSelected = (selectedNodeId === edge.get('target'));
+ const otherNodesSelected = this.props.hasSelectedNode && !sourceSelected && !targetSelected;
+ const sourceNoMatches = searchNodeMatches.get(edge.get('source'), makeMap()).isEmpty();
+ const targetNoMatches = searchNodeMatches.get(edge.get('target'), makeMap()).isEmpty();
+ const notMatched = this.props.searchQuery && (sourceNoMatches || targetNoMatches);
+ const sourceInNetwork = selectedNetworkNodesIds.contains(edge.get('source'));
+ const targetInNetwork = selectedNetworkNodesIds.contains(edge.get('target'));
+ const notInNetwork = this.props.selectedNetwork && (!sourceInNetwork || !targetInNetwork);
+ return edge.set('blurred', !edge.get('highlighted') && !edge.get('focused') &&
+ (otherNodesSelected || notMatched || notInNetwork));
+ }
+
+ edgeScaleDecorator(edge) {
+ return edge.set('scale', edge.get('focused') ? this.props.selectedScale : 1);
+ }
+
render() {
- const { hasSelectedNode, highlightedEdgeIds, layoutEdges, searchQuery,
- isAnimated, selectedScale, selectedNodeId, selectedNetwork, selectedNetworkNodesIds,
- searchNodeMatches } = this.props;
+ const { layoutEdges, isAnimated } = this.props;
+
+ const edgesToRender = layoutEdges.toIndexedSeq()
+ .map(this.edgeHighlightedDecorator)
+ .map(this.edgeFocusedDecorator)
+ .map(this.edgeBlurredDecorator)
+ .map(this.edgeScaleDecorator);
return (
- {layoutEdges.toIndexedSeq().map((edge) => {
- const sourceSelected = selectedNodeId === edge.get('source');
- const targetSelected = selectedNodeId === edge.get('target');
- const highlighted = highlightedEdgeIds.has(edge.get('id'));
- const focused = hasSelectedNode && (sourceSelected || targetSelected);
- const otherNodesSelected = hasSelectedNode && !sourceSelected && !targetSelected;
- const noMatches = searchQuery &&
- !(searchNodeMatches.has(edge.get('source')) &&
- searchNodeMatches.has(edge.get('target')));
- const noSelectedNetworks = selectedNetwork &&
- !(selectedNetworkNodesIds.contains(edge.get('source')) &&
- selectedNetworkNodesIds.contains(edge.get('target')));
- const blurred = !highlighted && (otherNodesSelected ||
- (!focused && noMatches) ||
- (!focused && noSelectedNetworks));
-
- return (
-
- );
- })}
+ {edgesToRender.map(edge => (
+
+ ))}
);
}
@@ -53,11 +80,11 @@ class NodesChartEdges extends React.Component {
export default connect(
state => ({
hasSelectedNode: hasSelectedNodeFn(state),
- highlightedEdgeIds: state.get('highlightedEdgeIds'),
- searchNodeMatches: currentTopologySearchNodeMatchesSelector(state),
- searchQuery: state.get('searchQuery'),
- selectedNetwork: state.get('selectedNetwork'),
+ searchNodeMatches: searchNodeMatchesSelector(state),
selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state),
+ searchQuery: state.get('searchQuery'),
+ highlightedEdgeIds: state.get('highlightedEdgeIds'),
+ selectedNetwork: state.get('selectedNetwork'),
selectedNodeId: state.get('selectedNodeId'),
})
)(NodesChartEdges);
diff --git a/client/app/scripts/charts/nodes-chart-nodes.js b/client/app/scripts/charts/nodes-chart-nodes.js
index 1a0a57a88..26e619b5e 100644
--- a/client/app/scripts/charts/nodes-chart-nodes.js
+++ b/client/app/scripts/charts/nodes-chart-nodes.js
@@ -1,77 +1,118 @@
import React from 'react';
import { connect } from 'react-redux';
+import { Map as makeMap } from 'immutable';
-import { nodeNetworksSelector, selectedNetworkNodesIdsSelector } from '../selectors/node-networks';
import { nodeMetricSelector } from '../selectors/node-metric';
-import { currentTopologySearchNodeMatchesSelector } from '../selectors/search';
+import { searchNodeMatchesSelector } from '../selectors/search';
+import { nodeNetworksSelector, selectedNetworkNodesIdsSelector } from '../selectors/node-networks';
import { getAdjacentNodes } from '../utils/topology-utils';
import NodeContainer from './node-container';
class NodesChartNodes extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+
+ this.nodeDisplayLayer = this.nodeDisplayLayer.bind(this);
+ // Node decorators
+ // TODO: Consider moving some of these one level up (or even to global selectors) so that
+ // other components, like NodesChartEdges, could read more info directly from the nodes.
+ this.nodeHighlightedDecorator = this.nodeHighlightedDecorator.bind(this);
+ this.nodeFocusedDecorator = this.nodeFocusedDecorator.bind(this);
+ this.nodeBlurredDecorator = this.nodeBlurredDecorator.bind(this);
+ this.nodeMatchesDecorator = this.nodeMatchesDecorator.bind(this);
+ this.nodeNetworksDecorator = this.nodeNetworksDecorator.bind(this);
+ this.nodeMetricDecorator = this.nodeMetricDecorator.bind(this);
+ this.nodeScaleDecorator = this.nodeScaleDecorator.bind(this);
+ }
+
+ nodeHighlightedDecorator(node) {
+ const nodeSelected = (this.props.selectedNodeId === node.get('id'));
+ const nodeHighlighted = this.props.highlightedNodeIds.has(node.get('id'));
+ return node.set('highlighted', nodeHighlighted || nodeSelected);
+ }
+
+ nodeFocusedDecorator(node) {
+ const nodeSelected = (this.props.selectedNodeId === node.get('id'));
+ const isNeighborOfSelected = this.props.neighborsOfSelectedNode.includes(node.get('id'));
+ return node.set('focused', nodeSelected || isNeighborOfSelected);
+ }
+
+ nodeBlurredDecorator(node) {
+ const belongsToNetwork = this.props.selectedNetworkNodesIds.contains(node.get('id'));
+ const noMatches = this.props.searchNodeMatches.get(node.get('id'), makeMap()).isEmpty();
+ const notMatched = (this.props.searchQuery && !node.get('highlighted') && noMatches);
+ const notFocused = (this.props.selectedNodeId && !node.get('focused'));
+ const notInNetwork = (this.props.selectedNetwork && !belongsToNetwork);
+ return node.set('blurred', notMatched || notFocused || notInNetwork);
+ }
+
+ nodeMatchesDecorator(node) {
+ return node.set('matches', this.props.searchNodeMatches.get(node.get('id')));
+ }
+
+ nodeNetworksDecorator(node) {
+ return node.set('networks', this.props.nodeNetworks.get(node.get('id')));
+ }
+
+ nodeMetricDecorator(node) {
+ return node.set('metric', this.props.nodeMetric.get(node.get('id')));
+ }
+
+ nodeScaleDecorator(node) {
+ return node.set('scale', node.get('focused') ? this.props.selectedScale : 1);
+ }
+
+ // make sure blurred nodes are in the background
+ nodeDisplayLayer(node) {
+ if (node.get('id') === this.props.mouseOverNodeId) {
+ return 3;
+ }
+ if (node.get('blurred') && !node.get('focused')) {
+ return 0;
+ }
+ if (node.get('highlighted')) {
+ return 2;
+ }
+ return 1;
+ }
+
render() {
- const { adjacentNodes, highlightedNodeIds, layoutNodes, isAnimated,
- mouseOverNodeId, nodeMetric, selectedScale, searchQuery, selectedNetwork,
- selectedNodeId, searchNodeMatches, nodeNetworks, selectedNetworkNodesIds } = this.props;
-
- // highlighter functions
- const setHighlighted = node => node.set('highlighted',
- highlightedNodeIds.has(node.get('id')) || selectedNodeId === node.get('id'));
- const setFocused = node => node.set('focused', selectedNodeId
- && (selectedNodeId === node.get('id')
- || (adjacentNodes && adjacentNodes.includes(node.get('id')))));
- const setBlurred = node => node.set('blurred',
- (selectedNodeId && !node.get('focused'))
- || (searchQuery && !searchNodeMatches.has(node.get('id')) && !node.get('highlighted'))
- || (selectedNetwork && !selectedNetworkNodesIds.contains(node.get('id'))));
-
- // make sure blurred nodes are in the background
- const sortNodes = (node) => {
- if (node.get('id') === mouseOverNodeId) {
- return 3;
- }
- if (node.get('blurred') && !node.get('focused')) {
- return 0;
- }
- if (node.get('highlighted')) {
- return 2;
- }
- return 1;
- };
+ const { layoutNodes, isAnimated } = this.props;
const nodesToRender = layoutNodes.toIndexedSeq()
- .map(setHighlighted)
- .map(setFocused)
- .map(setBlurred)
- .sortBy(sortNodes);
+ .map(this.nodeHighlightedDecorator)
+ .map(this.nodeFocusedDecorator)
+ .map(this.nodeBlurredDecorator)
+ .map(this.nodeMatchesDecorator)
+ .map(this.nodeNetworksDecorator)
+ .map(this.nodeMetricDecorator)
+ .map(this.nodeScaleDecorator)
+ .sortBy(this.nodeDisplayLayer);
return (
- {nodesToRender.map((node) => {
- const nodeScale = node.get('focused') ? selectedScale : 1;
- const nodeId = node.get('id');
- return (
-
- );
- })}
+ {nodesToRender.map(node => (
+
+ ))}
);
}
@@ -79,11 +120,11 @@ class NodesChartNodes extends React.Component {
function mapStateToProps(state) {
return {
- adjacentNodes: getAdjacentNodes(state),
nodeMetric: nodeMetricSelector(state),
nodeNetworks: nodeNetworksSelector(state),
- searchNodeMatches: currentTopologySearchNodeMatchesSelector(state),
+ searchNodeMatches: searchNodeMatchesSelector(state),
selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state),
+ neighborsOfSelectedNode: getAdjacentNodes(state),
highlightedNodeIds: state.get('highlightedNodeIds'),
mouseOverNodeId: state.get('mouseOverNodeId'),
selectedNetwork: state.get('selectedNetwork'),
diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js
index 2c9288b0c..9c4898110 100644
--- a/client/app/scripts/charts/nodes-chart.js
+++ b/client/app/scripts/charts/nodes-chart.js
@@ -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 = 150;
const ZOOM_CACHE_FIELDS = [
'panTranslateX', 'panTranslateY',
'zoomScale', 'minZoomScale', 'maxZoomScale'
diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js
index 158fa4b30..cfd9cd037 100644
--- a/client/app/scripts/charts/nodes-grid.js
+++ b/client/app/scripts/charts/nodes-grid.js
@@ -7,7 +7,7 @@ import NodeDetailsTable from '../components/node-details/node-details-table';
import { clickNode, sortOrderChanged } from '../actions/app-actions';
import { shownNodesSelector } from '../selectors/node-filters';
-import { currentTopologySearchNodeMatchesSelector } from '../selectors/search';
+import { searchNodeMatchesSelector } from '../selectors/search';
import { getNodeColor } from '../utils/color-utils';
@@ -115,7 +115,7 @@ class NodesGrid extends React.Component {
id: '',
nodes: nodes
.toList()
- .filter(n => !searchQuery || searchNodeMatches.has(n.get('id')))
+ .filter(n => !(searchQuery && searchNodeMatches.get(n.get('id'), makeMap()).isEmpty()))
.toJS(),
columns: getColumns(nodes)
};
@@ -149,7 +149,7 @@ function mapStateToProps(state) {
gridSortedDesc: state.get('gridSortedDesc'),
currentTopology: state.get('currentTopology'),
currentTopologyId: state.get('currentTopologyId'),
- searchNodeMatches: currentTopologySearchNodeMatchesSelector(state),
+ searchNodeMatches: searchNodeMatchesSelector(state),
searchQuery: state.get('searchQuery'),
selectedNodeId: state.get('selectedNodeId')
};
diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js
index d57ab02ba..5a9d9c319 100644
--- a/client/app/scripts/components/search.js
+++ b/client/app/scripts/components/search.js
@@ -4,7 +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 { searchMatchCountByTopologySelector } from '../selectors/search';
import { slugify } from '../utils/string-utils';
import { isTopologyEmpty } from '../utils/topology-utils';
import SearchItem from './search-item';
@@ -102,11 +102,11 @@ class Search extends React.Component {
}
render() {
- const { inputId = 'search', nodes, pinnedSearches, searchFocused,
- searchNodeMatches, searchQuery, topologiesLoaded, onClickHelp } = this.props;
+ const { nodes, pinnedSearches, searchFocused, searchMatchCountByTopology,
+ searchQuery, topologiesLoaded, onClickHelp, inputId = 'search' } = this.props;
const disabled = this.props.isTopologyEmpty;
- const matchCount = searchNodeMatches
- .reduce((count, topologyMatches) => count + topologyMatches.size, 0);
+ const matchCount = searchMatchCountByTopology
+ .reduce((count, topologyMatchCount) => count + topologyMatchCount, 0);
const showPinnedSearches = pinnedSearches.size > 0;
// manual clear (null) has priority, then props, then state
const value = this.state.value === null ? '' : this.state.value || searchQuery || '';
@@ -154,11 +154,11 @@ export default connect(
state => ({
nodes: state.get('nodes'),
isTopologyEmpty: isTopologyEmpty(state),
+ topologiesLoaded: state.get('topologiesLoaded'),
pinnedSearches: state.get('pinnedSearches'),
searchFocused: state.get('searchFocused'),
searchQuery: state.get('searchQuery'),
- searchNodeMatches: searchNodeMatchesSelector(state),
- topologiesLoaded: state.get('topologiesLoaded')
+ searchMatchCountByTopology: searchMatchCountByTopologySelector(state),
}),
{ blurSearch, doSearch, focusSearch, onClickHelp: showHelp }
)(Search);
diff --git a/client/app/scripts/components/topologies.js b/client/app/scripts/components/topologies.js
index 272799553..02bb50115 100644
--- a/client/app/scripts/components/topologies.js
+++ b/client/app/scripts/components/topologies.js
@@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
-import { searchNodeMatchesSelector } from '../selectors/search';
+import { searchMatchCountByTopologySelector } from '../selectors/search';
import { clickTopology } from '../actions/app-actions';
@@ -30,10 +30,9 @@ class Topologies extends React.Component {
}
renderSubTopology(subTopology) {
- const isActive = subTopology === this.props.currentTopology;
const topologyId = subTopology.get('id');
- const searchMatches = this.props.searchNodeMatches.get(subTopology.get('id'));
- const searchMatchCount = searchMatches ? searchMatches.size : 0;
+ const isActive = subTopology === this.props.currentTopology;
+ const searchMatchCount = this.props.searchMatchCountByTopology.get(topologyId) || 0;
const title = basicTopologyInfo(subTopology, searchMatchCount);
const className = classnames('topologies-sub-item', {
'topologies-sub-item-active': isActive,
@@ -53,8 +52,7 @@ class Topologies extends React.Component {
renderTopology(topology) {
const isActive = topology === this.props.currentTopology;
- const searchMatches = this.props.searchNodeMatches.get(topology.get('id'));
- const searchMatchCount = searchMatches ? searchMatches.size : 0;
+ const searchMatchCount = this.props.searchMatchCountByTopology.get(topology.get('id')) || 0;
const className = classnames('topologies-item-main', {
'topologies-item-main-active': isActive,
'topologies-item-main-matched': searchMatchCount
@@ -91,8 +89,8 @@ class Topologies extends React.Component {
function mapStateToProps(state) {
return {
topologies: state.get('topologies'),
- searchNodeMatches: searchNodeMatchesSelector(state),
- currentTopology: state.get('currentTopology')
+ currentTopology: state.get('currentTopology'),
+ searchMatchCountByTopology: searchMatchCountByTopologySelector(state),
};
}
diff --git a/client/app/scripts/reducers/__tests__/root-test.js b/client/app/scripts/reducers/__tests__/root-test.js
index a7dfbf860..250304c29 100644
--- a/client/app/scripts/reducers/__tests__/root-test.js
+++ b/client/app/scripts/reducers/__tests__/root-test.js
@@ -64,7 +64,7 @@ describe('RootReducer', () => {
}
],
stats: {
- edge_count: 319,
+ edge_count: 379,
filtered_nodes: 214,
node_count: 320,
nonpseudo_node_count: 320
diff --git a/client/app/scripts/selectors/__tests__/search-test.js b/client/app/scripts/selectors/__tests__/search-test.js
new file mode 100644
index 000000000..e825d5528
--- /dev/null
+++ b/client/app/scripts/selectors/__tests__/search-test.js
@@ -0,0 +1,118 @@
+import { fromJS, Map as makeMap } from 'immutable';
+
+const SearchSelectors = require('../search');
+
+describe('Search selectors', () => {
+ const nodeSets = {
+ someNodes: fromJS({
+ n1: {
+ id: 'n1',
+ label: 'node label 1',
+ metadata: [{
+ id: 'fieldId1',
+ label: 'Label 1',
+ value: 'value 1'
+ }],
+ metrics: [{
+ id: 'metric1',
+ label: 'Metric 1',
+ value: 1
+ }]
+ },
+ n2: {
+ id: 'n2',
+ label: 'node label 2',
+ metadata: [{
+ id: 'fieldId2',
+ label: 'Label 2',
+ value: 'value 2'
+ }],
+ tables: [{
+ id: 'metric1',
+ type: 'property-list',
+ rows: [{
+ id: 'label1',
+ entries: {
+ label: 'Label 1',
+ value: 'Label Value 1'
+ }
+ }, {
+ id: 'label2',
+ entries: {
+ label: 'Label 2',
+ value: 'Label Value 2'
+ }
+ }]
+ }, {
+ id: 'metric2',
+ type: 'multicolumn-table',
+ columns: [{
+ id: 'a',
+ label: 'A'
+ }, {
+ id: 'c',
+ label: 'C'
+ }],
+ rows: [{
+ id: 'row1',
+ entries: {
+ a: 'xxxa',
+ b: 'yyya',
+ c: 'zzz1'
+ }
+ }, {
+ id: 'row2',
+ entries: {
+ a: 'yyyb',
+ b: 'xxxb',
+ c: 'zzz2'
+ }
+ }, {
+ id: 'row3',
+ entries: {
+ a: 'Value 1',
+ b: 'Value 2',
+ c: 'Value 3'
+ }
+ }]
+ }],
+ },
+ })
+ };
+
+ describe('searchNodeMatchesSelector', () => {
+ const selector = SearchSelectors.searchNodeMatchesSelector;
+
+ it('should return no matches on an empty topology', () => {
+ const result = selector(fromJS({
+ nodes: makeMap(),
+ searchQuery: '',
+ }));
+ expect(result.filter(m => !m.isEmpty()).size).toEqual(0);
+ });
+
+ it('should return no matches when no query is present', () => {
+ const result = selector(fromJS({
+ nodes: nodeSets.someNodes,
+ searchQuery: '',
+ }));
+ expect(result.filter(m => !m.isEmpty()).size).toEqual(0);
+ });
+
+ it('should return no matches when query matches nothing', () => {
+ const result = selector(fromJS({
+ nodes: nodeSets.someNodes,
+ searchQuery: 'cantmatch',
+ }));
+ expect(result.filter(m => !m.isEmpty()).size).toEqual(0);
+ });
+
+ it('should return a matches when a query matches something', () => {
+ const result = selector(fromJS({
+ nodes: nodeSets.someNodes,
+ searchQuery: 'value 2',
+ }));
+ expect(result.filter(m => !m.isEmpty()).size).toEqual(1);
+ });
+ });
+});
diff --git a/client/app/scripts/selectors/node-networks.js b/client/app/scripts/selectors/node-networks.js
index 27f9fe6dd..830c2b98f 100644
--- a/client/app/scripts/selectors/node-networks.js
+++ b/client/app/scripts/selectors/node-networks.js
@@ -1,6 +1,6 @@
import { createSelector } from 'reselect';
import { createMapSelector } from 'reselect-map';
-import { fromJS, List as makeList } from 'immutable';
+import { fromJS, Map as makeMap, List as makeList } from 'immutable';
const extractNodeNetworksValue = (node) => {
@@ -36,10 +36,12 @@ export const availableNetworksSelector = createSelector(
.sortBy(m => m.get('label'))
);
+// NOTE: Don't use this selector directly in mapStateToProps
+// as it would get called too many times.
export const selectedNetworkNodesIdsSelector = createSelector(
[
state => state.get('networkNodes'),
state => state.get('selectedNetwork'),
],
- (networkNodes, selectedNetwork) => networkNodes.get(selectedNetwork)
+ (networkNodes, selectedNetwork) => networkNodes.get(selectedNetwork, makeMap())
);
diff --git a/client/app/scripts/selectors/nodes-chart-focus.js b/client/app/scripts/selectors/nodes-chart-focus.js
index a5b4ce637..c9da89261 100644
--- a/client/app/scripts/selectors/nodes-chart-focus.js
+++ b/client/app/scripts/selectors/nodes-chart-focus.js
@@ -13,23 +13,13 @@ const radiusDensity = scaleThreshold()
.domain([3, 6])
.range([2.5, 3.5, 3]);
-
-const layoutNodesSelector = state => state.layoutNodes;
-const layoutEdgesSelector = state => state.layoutEdges;
-const stateWidthSelector = state => state.width;
-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 propsMarginsSelector = (_, props) => props.margins;
+// TODO: Make all the selectors below pure (so that they only depend on the global state).
// The narrower dimension of the viewport, used for scaling.
const viewportExpanseSelector = createSelector(
[
- stateWidthSelector,
- stateHeightSelector,
+ state => state.width,
+ state => state.height,
],
(width, height) => Math.min(width, height)
);
@@ -38,12 +28,12 @@ const viewportExpanseSelector = createSelector(
// panel is open), used for focusing the selected node.
const viewportCenterSelector = createSelector(
[
- stateWidthSelector,
- stateHeightSelector,
- stateTranslateXSelector,
- stateTranslateYSelector,
- stateScaleSelector,
- propsMarginsSelector,
+ state => state.width,
+ state => state.height,
+ state => state.panTranslateX,
+ state => state.panTranslateY,
+ state => state.zoomScale,
+ (_, props) => props.margins,
],
(width, height, translateX, translateY, scale, margins) => {
const viewportHalfWidth = ((width + margins.left) - DETAILS_PANEL_WIDTH) / 2;
@@ -57,10 +47,11 @@ const viewportCenterSelector = createSelector(
// List of all the adjacent nodes to the selected
// one, excluding itself (in case of loops).
+// TODO: Use createMapSelector here instead.
const selectedNodeNeighborsIdsSelector = createSelector(
[
- propsSelectedNodeIdSelector,
- inputNodesSelector,
+ (_, props) => props.selectedNodeId,
+ (_, props) => props.nodes,
],
(selectedNodeId, nodes) => {
let adjacentNodes = makeSet();
@@ -84,11 +75,11 @@ const selectedNodeNeighborsIdsSelector = createSelector(
const selectedNodesLayoutSettingsSelector = createSelector(
[
+ state => state.zoomScale,
selectedNodeNeighborsIdsSelector,
viewportExpanseSelector,
- stateScaleSelector,
],
- (circularNodesIds, viewportExpanse, scale) => {
+ (scale, circularNodesIds, viewportExpanse) => {
const circularNodesCount = circularNodesIds.length;
// Here we calculate the zoom factor of the nodes that get selected into focus.
@@ -114,14 +105,14 @@ const selectedNodesLayoutSettingsSelector = createSelector(
export const layoutWithSelectedNode = createSelector(
[
- layoutNodesSelector,
- layoutEdgesSelector,
+ state => state.layoutNodes,
+ state => state.layoutEdges,
+ (_, props) => props.selectedNodeId,
viewportCenterSelector,
- propsSelectedNodeIdSelector,
selectedNodeNeighborsIdsSelector,
selectedNodesLayoutSettingsSelector,
],
- (layoutNodes, layoutEdges, viewportCenter, selectedNodeId, neighborsIds, layoutSettings) => {
+ (layoutNodes, layoutEdges, selectedNodeId, viewportCenter, neighborsIds, layoutSettings) => {
// Do nothing if the layout doesn't contain the selected node anymore.
if (!layoutNodes.has(selectedNodeId)) {
return {};
diff --git a/client/app/scripts/selectors/nodes-chart-layout.js b/client/app/scripts/selectors/nodes-chart-layout.js
index 894c97492..604b81dfd 100644
--- a/client/app/scripts/selectors/nodes-chart-layout.js
+++ b/client/app/scripts/selectors/nodes-chart-layout.js
@@ -1,5 +1,5 @@
import debug from 'debug';
-import { createSelector } from 'reselect';
+import { createSelector, createStructuredSelector } from 'reselect';
import { Map as makeMap } from 'immutable';
import timely from 'timely';
@@ -9,15 +9,6 @@ import { doLayout } from '../charts/nodes-layout';
const log = debug('scope:nodes-chart');
-const stateWidthSelector = state => state.width;
-const stateHeightSelector = state => state.height;
-const inputNodesSelector = (_, props) => props.nodes;
-const propsMarginsSelector = (_, props) => props.margins;
-const forceRelayoutSelector = (_, props) => props.forceRelayout;
-const topologyIdSelector = (_, props) => props.topologyId;
-const topologyOptionsSelector = (_, props) => props.topologyOptions;
-
-
function initEdgesFromNodes(nodes) {
let edges = makeMap();
@@ -47,23 +38,20 @@ function initEdgesFromNodes(nodes) {
return edges;
}
-const layoutOptionsSelector = createSelector(
- [
- stateWidthSelector,
- stateHeightSelector,
- propsMarginsSelector,
- forceRelayoutSelector,
- topologyIdSelector,
- topologyOptionsSelector,
- ],
- (width, height, margins, forceRelayout, topologyId, topologyOptions) => (
- { width, height, margins, forceRelayout, topologyId, topologyOptions }
- )
-);
+// TODO: Make all the selectors below pure (so that they only depend on the global state).
+
+const layoutOptionsSelector = createStructuredSelector({
+ width: state => state.width,
+ height: state => state.height,
+ margins: (_, props) => props.margins,
+ forceRelayout: (_, props) => props.forceRelayout,
+ topologyId: (_, props) => props.topologyId,
+ topologyOptions: (_, props) => props.topologyOptions,
+});
export const graphLayout = createSelector(
[
- inputNodesSelector,
+ (_, props) => props.nodes,
layoutOptionsSelector,
],
(nodes, options) => {
diff --git a/client/app/scripts/selectors/nodes-chart-zoom.js b/client/app/scripts/selectors/nodes-chart-zoom.js
index ede1e9c45..d95dbc46c 100644
--- a/client/app/scripts/selectors/nodes-chart-zoom.js
+++ b/client/app/scripts/selectors/nodes-chart-zoom.js
@@ -3,23 +3,19 @@ import { createSelector } from 'reselect';
import { NODE_BASE_SIZE } from '../constants/styles';
import { zoomCacheKey } from '../utils/topology-utils';
-const layoutNodesSelector = state => state.layoutNodes;
-const stateWidthSelector = state => state.width;
-const stateHeightSelector = state => state.height;
-const propsMarginsSelector = (_, props) => props.margins;
-const cachedZoomStateSelector = (state, props) => state.zoomCache[zoomCacheKey(props)];
+// TODO: Make all the selectors below pure (so that they only depend on the global state).
const viewportWidthSelector = createSelector(
[
- stateWidthSelector,
- propsMarginsSelector,
+ state => state.width,
+ (_, props) => props.margins,
],
(width, margins) => width - margins.left - margins.right
);
const viewportHeightSelector = createSelector(
[
- stateHeightSelector,
- propsMarginsSelector,
+ state => state.height,
+ (_, props) => props.margins,
],
(height, margins) => height - margins.top
);
@@ -27,12 +23,12 @@ const viewportHeightSelector = createSelector(
// Compute the default zoom settings for the given graph layout.
const defaultZoomSelector = createSelector(
[
- layoutNodesSelector,
+ state => state.layoutNodes,
+ (_, props) => props.margins,
viewportWidthSelector,
viewportHeightSelector,
- propsMarginsSelector,
],
- (layoutNodes, width, height, margins) => {
+ (layoutNodes, margins, width, height) => {
if (layoutNodes.size === 0) {
return {};
}
@@ -67,7 +63,7 @@ const defaultZoomSelector = createSelector(
// otherwise use the default zoom options computed from the graph layout.
export const topologyZoomState = createSelector(
[
- cachedZoomStateSelector,
+ (state, props) => state.zoomCache[zoomCacheKey(props)],
defaultZoomSelector,
],
(cachedZoomState, defaultZoomState) => cachedZoomState || defaultZoomState
diff --git a/client/app/scripts/selectors/search.js b/client/app/scripts/selectors/search.js
index 354bc6677..00b16b1bb 100644
--- a/client/app/scripts/selectors/search.js
+++ b/client/app/scripts/selectors/search.js
@@ -2,7 +2,7 @@ import { createSelector } from 'reselect';
import { createMapSelector } from 'reselect-map';
import { Map as makeMap } from 'immutable';
-import { parseQuery, searchTopology, getSearchableFields } from '../utils/search-utils';
+import { parseQuery, searchNode, searchTopology, getSearchableFields } from '../utils/search-utils';
const parsedSearchQuerySelector = createSelector(
@@ -13,27 +13,26 @@ const parsedSearchQuerySelector = createSelector(
);
export const searchNodeMatchesSelector = createMapSelector(
+ [
+ state => state.get('nodes'),
+ parsedSearchQuerySelector,
+ ],
+ (node, parsed) => (parsed ? searchNode(node, parsed) : makeMap())
+);
+
+export const searchMatchCountByTopologySelector = createMapSelector(
[
state => state.get('nodesByTopology'),
parsedSearchQuerySelector,
],
// TODO: Bring map selectors one level deeper here so that `searchTopology` is
// not executed against all the topology nodes when the nodes delta is small.
- (nodes, parsed) => (parsed ? searchTopology(nodes, parsed) : makeMap())
-);
-
-export const currentTopologySearchNodeMatchesSelector = createSelector(
- [
- state => state.get('currentTopologyId'),
- searchNodeMatchesSelector,
- ],
- (currentTopologyId, nodesByTopology) => nodesByTopology.get(currentTopologyId) || makeMap()
+ (nodes, parsed) => (parsed ? searchTopology(nodes, parsed).size : 0)
);
export const searchableFieldsSelector = createSelector(
[
state => state.get('nodes'),
],
- // TODO: Bring this function in the selectors.
getSearchableFields
);
diff --git a/client/app/scripts/utils/search-utils.js b/client/app/scripts/utils/search-utils.js
index 51ec0aa1d..ee1629d61 100644
--- a/client/app/scripts/utils/search-utils.js
+++ b/client/app/scripts/utils/search-utils.js
@@ -117,70 +117,80 @@ function findNodeMatchMetric(nodeMatches, keyPath, fieldValue, fieldLabel, metri
return nodeMatches;
}
-
-export function searchTopology(nodes, { prefix, query, metric, comp, value }) {
+export function searchNode(node, { prefix, query, metric, comp, value }) {
let nodeMatches = makeMap();
+
+ if (query) {
+ // top level fields
+ SEARCH_FIELDS.forEach((field, label) => {
+ const keyPath = [label];
+ if (node.has(field)) {
+ nodeMatches = findNodeMatch(nodeMatches, keyPath, node.get(field),
+ query, prefix, label);
+ }
+ });
+
+ // metadata
+ if (node.get('metadata')) {
+ node.get('metadata').forEach((field) => {
+ const keyPath = ['metadata', field.get('id')];
+ nodeMatches = findNodeMatch(nodeMatches, keyPath, field.get('value'),
+ query, prefix, field.get('label'), field.get('truncate'));
+ });
+ }
+
+ // parents and relatives
+ if (node.get('parents')) {
+ node.get('parents').forEach((parent) => {
+ const keyPath = ['parents', parent.get('id')];
+ nodeMatches = findNodeMatch(nodeMatches, keyPath, parent.get('label'),
+ query, prefix, parent.get('topologyId'));
+ });
+ }
+
+ // property lists
+ (node.get('tables') || []).filter(isPropertyList).forEach((propertyList) => {
+ (propertyList.get('rows') || []).forEach((row) => {
+ const entries = row.get('entries');
+ const keyPath = ['property-lists', row.get('id')];
+ nodeMatches = findNodeMatch(nodeMatches, keyPath, entries.get('value'),
+ query, prefix, entries.get('label'));
+ });
+ });
+
+ // generic tables
+ (node.get('tables') || []).filter(isGenericTable).forEach((table) => {
+ (table.get('rows') || []).forEach((row) => {
+ table.get('columns').forEach((column) => {
+ const val = row.get('entries').get(column.get('id'));
+ const keyPath = ['tables', genericTableEntryKey(row, column)];
+ nodeMatches = findNodeMatch(nodeMatches, keyPath, val, query);
+ });
+ });
+ });
+ } else if (metric) {
+ const metrics = node.get('metrics');
+ if (metrics) {
+ metrics.forEach((field) => {
+ const keyPath = ['metrics', field.get('id')];
+ nodeMatches = findNodeMatchMetric(nodeMatches, keyPath, field.get('value'),
+ field.get('label'), metric, comp, value);
+ });
+ }
+ }
+
+ return nodeMatches;
+}
+
+export function searchTopology(nodes, parsedQuery) {
+ let nodesMatches = makeMap();
nodes.forEach((node, nodeId) => {
- if (query) {
- // top level fields
- SEARCH_FIELDS.forEach((field, label) => {
- const keyPath = [nodeId, label];
- if (node.has(field)) {
- nodeMatches = findNodeMatch(nodeMatches, keyPath, node.get(field),
- query, prefix, label);
- }
- });
-
- // metadata
- if (node.get('metadata')) {
- node.get('metadata').forEach((field) => {
- const keyPath = [nodeId, 'metadata', field.get('id')];
- nodeMatches = findNodeMatch(nodeMatches, keyPath, field.get('value'),
- query, prefix, field.get('label'), field.get('truncate'));
- });
- }
-
- // parents and relatives
- if (node.get('parents')) {
- node.get('parents').forEach((parent) => {
- const keyPath = [nodeId, 'parents', parent.get('id')];
- nodeMatches = findNodeMatch(nodeMatches, keyPath, parent.get('label'),
- query, prefix, parent.get('topologyId'));
- });
- }
-
- // property lists
- (node.get('tables') || []).filter(isPropertyList).forEach((propertyList) => {
- (propertyList.get('rows') || []).forEach((row) => {
- const entries = row.get('entries');
- const keyPath = [nodeId, 'property-lists', row.get('id')];
- nodeMatches = findNodeMatch(nodeMatches, keyPath, entries.get('value'),
- query, prefix, entries.get('label'));
- });
- });
-
- // generic tables
- (node.get('tables') || []).filter(isGenericTable).forEach((table) => {
- (table.get('rows') || []).forEach((row) => {
- table.get('columns').forEach((column) => {
- const val = row.get('entries').get(column.get('id'));
- const keyPath = [nodeId, 'tables', genericTableEntryKey(row, column)];
- nodeMatches = findNodeMatch(nodeMatches, keyPath, val, query);
- });
- });
- });
- } else if (metric) {
- const metrics = node.get('metrics');
- if (metrics) {
- metrics.forEach((field) => {
- const keyPath = [nodeId, 'metrics', field.get('id')];
- nodeMatches = findNodeMatchMetric(nodeMatches, keyPath, field.get('value'),
- field.get('label'), metric, comp, value);
- });
- }
+ const nodeMatches = searchNode(node, parsedQuery);
+ if (!nodeMatches.isEmpty()) {
+ nodesMatches = nodesMatches.set(nodeId, nodeMatches);
}
});
- return nodeMatches;
+ return nodesMatches;
}
/**
diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js
index 80aca9e5a..f70810578 100644
--- a/client/app/scripts/utils/topology-utils.js
+++ b/client/app/scripts/utils/topology-utils.js
@@ -174,11 +174,6 @@ export function getCurrentTopologyUrl(state) {
return state.getIn(['currentTopology', 'url']);
}
-export function isNodeMatchingQuery(node, query) {
- return node.get('label').includes(query) ||
- node.get('labelMinor').includes(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'))) > 1000;
diff --git a/client/package.json b/client/package.json
index 4ce6c23b7..db2b8096d 100644
--- a/client/package.json
+++ b/client/package.json
@@ -29,7 +29,6 @@
"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",
@@ -72,6 +71,7 @@
"json-loader": "0.5.4",
"node-sass": "3.13.1",
"postcss-loader": "1.2.0",
+ "react-addons-perf": "15.4.1",
"redux-devtools": "3.3.1",
"redux-devtools-dock-monitor": "1.1.1",
"redux-devtools-log-monitor": "1.1.1",