import React from 'react'; import classNames from 'classnames'; import { connect } from 'react-redux'; import { fromJS, Map as makeMap, List as makeList } from 'immutable'; import theme from 'weaveworks-ui-components/lib/theme'; import NodeContainer from './node-container'; import EdgeContainer from './edge-container'; import { getAdjacentNodes, hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils'; import { graphExceedsComplexityThreshSelector } from '../selectors/topology'; import { nodeNetworksSelector, selectedNetworkNodesIdsSelector } from '../selectors/node-networks'; import { searchNodeMatchesSelector } from '../selectors/search'; import { nodeMetricSelector } from '../selectors/node-metric'; import { highlightedNodeIdsSelector, highlightedEdgeIdsSelector } from '../selectors/graph-view/decorators'; import { selectedScaleSelector, layoutNodesSelector, layoutEdgesSelector } from '../selectors/graph-view/layout'; import { NODE_BASE_SIZE } from '../constants/styles'; import { BLURRED_EDGES_LAYER, BLURRED_NODES_LAYER, NORMAL_EDGES_LAYER, NORMAL_NODES_LAYER, HIGHLIGHTED_EDGES_LAYER, HIGHLIGHTED_NODES_LAYER, HOVERED_EDGES_LAYER, HOVERED_NODES_LAYER, } from '../constants/naming'; class NodesChartElements extends React.Component { constructor(props, context) { super(props, context); this.renderNode = this.renderNode.bind(this); this.renderEdge = this.renderEdge.bind(this); this.renderElement = this.renderElement.bind(this); this.nodeDisplayLayer = this.nodeDisplayLayer.bind(this); this.edgeDisplayLayer = this.edgeDisplayLayer.bind(this); // Node decorators 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); // 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); this.state={ nodeColor:'#5B8FF9' } } nodeDisplayLayer(node) { if (node.get('id') === this.props.mouseOverNodeId) { return HOVERED_NODES_LAYER; } if (node.get('blurred') && !node.get('focused')) { return BLURRED_NODES_LAYER; } if (node.get('highlighted')) { return HIGHLIGHTED_NODES_LAYER; } return NORMAL_NODES_LAYER; } edgeDisplayLayer(edge) { if (edge.get('id') === this.props.mouseOverEdgeId) { return HOVERED_EDGES_LAYER; } if (edge.get('blurred') && !edge.get('focused')) { return BLURRED_EDGES_LAYER; } if (edge.get('highlighted')) { return HIGHLIGHTED_EDGES_LAYER; } return NORMAL_EDGES_LAYER; } 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); } 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); } renderNode(node) { const { isAnimated } = this.props; // old versions of scope reports have a node shape of `storagesheet` // if so, normalise to `sheet` const shape = node.get('shape') === 'storagesheet' ? 'sheet' : node.get('shape'); return ( ); } renderEdge(edge) { const { isAnimated } = this.props; return ( ); } renderOverlay(element) { // NOTE: This piece of code is a bit hacky - as we can't set the absolute coords for the // SVG element, we set the zoom level high enough that we're sure it covers the screen. const className = classNames('nodes-chart-overlay', { active: element.get('isActive') }); const scale = (this.props.selectedScale || 1) * 100000; return ( ); } renderElement(element) { if (element.get('isOverlay')) { return this.renderOverlay(element); } // This heuristics is not ideal but it works. return element.get('points') ? this.renderEdge(element) : this.renderNode(element); } render() { const nodes = this.props.layoutNodes.toIndexedSeq() .map(this.nodeHighlightedDecorator) .map(this.nodeFocusedDecorator) .map(this.nodeBlurredDecorator) .map(this.nodeMatchesDecorator) .map(this.nodeNetworksDecorator) .map(this.nodeMetricDecorator) .map(this.nodeScaleDecorator) .groupBy(this.nodeDisplayLayer); const edges = this.props.layoutEdges.toIndexedSeq() .map(this.edgeHighlightedDecorator) .map(this.edgeFocusedDecorator) .map(this.edgeBlurredDecorator) .map(this.edgeScaleDecorator) .groupBy(this.edgeDisplayLayer); // NOTE: The elements need to be arranged into a single array outside // of DOM structure for React rendering engine to do smart rearrangements // without unnecessary re-rendering of the elements themselves. So e.g. // rendering the element layers individually below would be significantly slower. const orderedElements = makeList([ edges.get(BLURRED_EDGES_LAYER, makeList()), nodes.get(BLURRED_NODES_LAYER, makeList()), fromJS([{ isActive: !!nodes.get(BLURRED_NODES_LAYER), isOverlay: true }]), edges.get(NORMAL_EDGES_LAYER, makeList()), nodes.get(NORMAL_NODES_LAYER, makeList()), edges.get(HIGHLIGHTED_EDGES_LAYER, makeList()), nodes.get(HIGHLIGHTED_NODES_LAYER, makeList()), edges.get(HOVERED_EDGES_LAYER, makeList()), nodes.get(HOVERED_NODES_LAYER, makeList()), ]).flatten(true); return ( {orderedElements.map(this.renderElement)} ); } } function mapStateToProps(state) { return { contrastMode: state.get('contrastMode'), hasSelectedNode: hasSelectedNodeFn(state), highlightedEdgeIds: highlightedEdgeIdsSelector(state), highlightedNodeIds: highlightedNodeIdsSelector(state), isAnimated: !graphExceedsComplexityThreshSelector(state), layoutEdges: layoutEdgesSelector(state), layoutNodes: layoutNodesSelector(state), mouseOverEdgeId: state.get('mouseOverEdgeId'), mouseOverNodeId: state.get('mouseOverNodeId'), neighborsOfSelectedNode: getAdjacentNodes(state), nodeMetric: nodeMetricSelector(state), nodeNetworks: nodeNetworksSelector(state), searchNodeMatches: searchNodeMatchesSelector(state), searchQuery: state.get('searchQuery'), selectedNetwork: state.get('selectedNetwork'), selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state), selectedNodeId: state.get('selectedNodeId'), selectedScale: selectedScaleSelector(state), }; } export default connect(mapStateToProps)(NodesChartElements);