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