Polished the selectors code and fixed failing tests.

This commit is contained in:
Filip Barl
2017-02-13 17:02:39 +01:00
parent b9ba83ffca
commit 6d0066cd38
16 changed files with 429 additions and 264 deletions

View File

@@ -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 (
<g className="nodes-chart-edges">
{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 (
<EdgeContainer
key={edge.get('id')}
id={edge.get('id')}
source={edge.get('source')}
target={edge.get('target')}
waypoints={edge.get('points')}
scale={focused ? selectedScale : 1}
isAnimated={isAnimated}
blurred={blurred}
focused={focused}
highlighted={highlighted}
/>
);
})}
{edgesToRender.map(edge => (
<EdgeContainer
key={edge.get('id')}
id={edge.get('id')}
source={edge.get('source')}
target={edge.get('target')}
waypoints={edge.get('points')}
highlighted={edge.get('highlighted')}
focused={edge.get('focused')}
blurred={edge.get('blurred')}
scale={edge.get('scale')}
isAnimated={isAnimated}
/>
))}
</g>
);
}
@@ -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);

View File

@@ -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 (
<g className="nodes-chart-nodes">
{nodesToRender.map((node) => {
const nodeScale = node.get('focused') ? selectedScale : 1;
const nodeId = node.get('id');
return (
<NodeContainer
matches={searchNodeMatches.get(nodeId)}
networks={nodeNetworks.get(nodeId)}
metric={nodeMetric.get(nodeId)}
blurred={node.get('blurred')}
focused={node.get('focused')}
highlighted={node.get('highlighted')}
shape={node.get('shape')}
stack={node.get('stack')}
key={node.get('id')}
id={node.get('id')}
label={node.get('label')}
labelMinor={node.get('labelMinor')}
pseudo={node.get('pseudo')}
rank={node.get('rank')}
dx={node.get('x')}
dy={node.get('y')}
scale={nodeScale}
isAnimated={isAnimated}
/>
);
})}
{nodesToRender.map(node => (
<NodeContainer
matches={node.get('matches')}
networks={node.get('networks')}
metric={node.get('metric')}
blurred={node.get('blurred')}
focused={node.get('focused')}
highlighted={node.get('highlighted')}
shape={node.get('shape')}
stack={node.get('stack')}
key={node.get('id')}
id={node.get('id')}
label={node.get('label')}
labelMinor={node.get('labelMinor')}
pseudo={node.get('pseudo')}
rank={node.get('rank')}
dx={node.get('x')}
dy={node.get('y')}
scale={node.get('scale')}
isAnimated={isAnimated}
/>
))}
</g>
);
}
@@ -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'),

View File

@@ -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'

View File

@@ -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')
};

View File

@@ -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);

View File

@@ -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),
};
}

View File

@@ -64,7 +64,7 @@ describe('RootReducer', () => {
}
],
stats: {
edge_count: 319,
edge_count: 379,
filtered_nodes: 214,
node_count: 320,
nonpseudo_node_count: 320

View File

@@ -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);
});
});
});

View File

@@ -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())
);

View File

@@ -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 {};

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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
);

View File

@@ -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;
}
/**

View File

@@ -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;

View File

@@ -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",