mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-02 17:50:39 +00:00
Polished the selectors code and fixed failing tests.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('RootReducer', () => {
|
||||
}
|
||||
],
|
||||
stats: {
|
||||
edge_count: 319,
|
||||
edge_count: 379,
|
||||
filtered_nodes: 214,
|
||||
node_count: 320,
|
||||
nonpseudo_node_count: 320
|
||||
|
||||
118
client/app/scripts/selectors/__tests__/search-test.js
Normal file
118
client/app/scripts/selectors/__tests__/search-test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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())
|
||||
);
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user