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