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