nodes-chart-elements.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import React from 'react';
  2. import classNames from 'classnames';
  3. import { connect } from 'react-redux';
  4. import { fromJS, Map as makeMap, List as makeList } from 'immutable';
  5. import theme from 'weaveworks-ui-components/lib/theme';
  6. import NodeContainer from './node-container';
  7. import EdgeContainer from './edge-container';
  8. import { getAdjacentNodes, hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils';
  9. import { graphExceedsComplexityThreshSelector } from '../selectors/topology';
  10. import { nodeNetworksSelector, selectedNetworkNodesIdsSelector } from '../selectors/node-networks';
  11. import { searchNodeMatchesSelector } from '../selectors/search';
  12. import { nodeMetricSelector } from '../selectors/node-metric';
  13. import {
  14. highlightedNodeIdsSelector,
  15. highlightedEdgeIdsSelector
  16. } from '../selectors/graph-view/decorators';
  17. import {
  18. selectedScaleSelector,
  19. layoutNodesSelector,
  20. layoutEdgesSelector
  21. } from '../selectors/graph-view/layout';
  22. import { NODE_BASE_SIZE } from '../constants/styles';
  23. import {
  24. BLURRED_EDGES_LAYER,
  25. BLURRED_NODES_LAYER,
  26. NORMAL_EDGES_LAYER,
  27. NORMAL_NODES_LAYER,
  28. HIGHLIGHTED_EDGES_LAYER,
  29. HIGHLIGHTED_NODES_LAYER,
  30. HOVERED_EDGES_LAYER,
  31. HOVERED_NODES_LAYER,
  32. } from '../constants/naming';
  33. class NodesChartElements extends React.Component {
  34. constructor(props, context) {
  35. super(props, context);
  36. this.renderNode = this.renderNode.bind(this);
  37. this.renderEdge = this.renderEdge.bind(this);
  38. this.renderElement = this.renderElement.bind(this);
  39. this.nodeDisplayLayer = this.nodeDisplayLayer.bind(this);
  40. this.edgeDisplayLayer = this.edgeDisplayLayer.bind(this);
  41. // Node decorators
  42. this.nodeHighlightedDecorator = this.nodeHighlightedDecorator.bind(this);
  43. this.nodeFocusedDecorator = this.nodeFocusedDecorator.bind(this);
  44. this.nodeBlurredDecorator = this.nodeBlurredDecorator.bind(this);
  45. this.nodeMatchesDecorator = this.nodeMatchesDecorator.bind(this);
  46. this.nodeNetworksDecorator = this.nodeNetworksDecorator.bind(this);
  47. this.nodeMetricDecorator = this.nodeMetricDecorator.bind(this);
  48. this.nodeScaleDecorator = this.nodeScaleDecorator.bind(this);
  49. // Edge decorators
  50. this.edgeFocusedDecorator = this.edgeFocusedDecorator.bind(this);
  51. this.edgeBlurredDecorator = this.edgeBlurredDecorator.bind(this);
  52. this.edgeHighlightedDecorator = this.edgeHighlightedDecorator.bind(this);
  53. this.edgeScaleDecorator = this.edgeScaleDecorator.bind(this);
  54. this.state={
  55. nodeColor:'#5B8FF9'
  56. }
  57. }
  58. nodeDisplayLayer(node) {
  59. if (node.get('id') === this.props.mouseOverNodeId) {
  60. return HOVERED_NODES_LAYER;
  61. } if (node.get('blurred') && !node.get('focused')) {
  62. return BLURRED_NODES_LAYER;
  63. } if (node.get('highlighted')) {
  64. return HIGHLIGHTED_NODES_LAYER;
  65. }
  66. return NORMAL_NODES_LAYER;
  67. }
  68. edgeDisplayLayer(edge) {
  69. if (edge.get('id') === this.props.mouseOverEdgeId) {
  70. return HOVERED_EDGES_LAYER;
  71. } if (edge.get('blurred') && !edge.get('focused')) {
  72. return BLURRED_EDGES_LAYER;
  73. } if (edge.get('highlighted')) {
  74. return HIGHLIGHTED_EDGES_LAYER;
  75. }
  76. return NORMAL_EDGES_LAYER;
  77. }
  78. nodeHighlightedDecorator(node) {
  79. const nodeSelected = (this.props.selectedNodeId === node.get('id'));
  80. const nodeHighlighted = this.props.highlightedNodeIds.has(node.get('id'));
  81. return node.set('highlighted', nodeHighlighted || nodeSelected);
  82. }
  83. nodeFocusedDecorator(node) {
  84. const nodeSelected = (this.props.selectedNodeId === node.get('id'));
  85. const isNeighborOfSelected = this.props.neighborsOfSelectedNode.includes(node.get('id'));
  86. return node.set('focused', nodeSelected || isNeighborOfSelected);
  87. }
  88. nodeBlurredDecorator(node) {
  89. const belongsToNetwork = this.props.selectedNetworkNodesIds.contains(node.get('id'));
  90. const noMatches = this.props.searchNodeMatches.get(node.get('id'), makeMap()).isEmpty();
  91. const notMatched = (this.props.searchQuery && !node.get('highlighted') && noMatches);
  92. const notFocused = (this.props.selectedNodeId && !node.get('focused'));
  93. const notInNetwork = (this.props.selectedNetwork && !belongsToNetwork);
  94. return node.set('blurred', notMatched || notFocused || notInNetwork);
  95. }
  96. nodeMatchesDecorator(node) {
  97. return node.set('matches', this.props.searchNodeMatches.get(node.get('id')));
  98. }
  99. nodeNetworksDecorator(node) {
  100. return node.set('networks', this.props.nodeNetworks.get(node.get('id')));
  101. }
  102. nodeMetricDecorator(node) {
  103. return node.set('metric', this.props.nodeMetric.get(node.get('id')));
  104. }
  105. nodeScaleDecorator(node) {
  106. return node.set('scale', node.get('focused') ? this.props.selectedScale : 1);
  107. }
  108. edgeHighlightedDecorator(edge) {
  109. return edge.set('highlighted', this.props.highlightedEdgeIds.has(edge.get('id')));
  110. }
  111. edgeFocusedDecorator(edge) {
  112. const sourceSelected = (this.props.selectedNodeId === edge.get('source'));
  113. const targetSelected = (this.props.selectedNodeId === edge.get('target'));
  114. return edge.set('focused', this.props.hasSelectedNode && (sourceSelected || targetSelected));
  115. }
  116. edgeBlurredDecorator(edge) {
  117. const { selectedNodeId, searchNodeMatches, selectedNetworkNodesIds } = this.props;
  118. const sourceSelected = (selectedNodeId === edge.get('source'));
  119. const targetSelected = (selectedNodeId === edge.get('target'));
  120. const otherNodesSelected = this.props.hasSelectedNode && !sourceSelected && !targetSelected;
  121. const sourceNoMatches = searchNodeMatches.get(edge.get('source'), makeMap()).isEmpty();
  122. const targetNoMatches = searchNodeMatches.get(edge.get('target'), makeMap()).isEmpty();
  123. const notMatched = this.props.searchQuery && (sourceNoMatches || targetNoMatches);
  124. const sourceInNetwork = selectedNetworkNodesIds.contains(edge.get('source'));
  125. const targetInNetwork = selectedNetworkNodesIds.contains(edge.get('target'));
  126. const notInNetwork = this.props.selectedNetwork && (!sourceInNetwork || !targetInNetwork);
  127. return edge.set('blurred', !edge.get('highlighted') && !edge.get('focused')
  128. && (otherNodesSelected || notMatched || notInNetwork));
  129. }
  130. edgeScaleDecorator(edge) {
  131. return edge.set('scale', edge.get('focused') ? this.props.selectedScale : 1);
  132. }
  133. renderNode(node) {
  134. const { isAnimated } = this.props;
  135. // old versions of scope reports have a node shape of `storagesheet`
  136. // if so, normalise to `sheet`
  137. const shape = node.get('shape') === 'storagesheet' ? 'sheet' : node.get('shape');
  138. return (
  139. <NodeContainer
  140. matches={node.get('matches')}
  141. networks={node.get('networks')}
  142. metric={node.get('metric')}
  143. focused={node.get('focused')}
  144. highlighted={node.get('highlighted')}
  145. shape={shape}
  146. tag={node.get('tag')}
  147. stacked={node.get('stack')}
  148. key={node.get('id')}
  149. id={node.get('id')}
  150. label={node.get('label')}
  151. labelMinor={node.get('labelMinor')}
  152. pseudo={node.get('pseudo')}
  153. rank={node.get('rank')}
  154. x={node.get('x')}
  155. y={node.get('y')}
  156. size={node.get('scale') * NODE_BASE_SIZE}
  157. isAnimated={isAnimated}
  158. color={node.get('color')}
  159. />
  160. );
  161. }
  162. renderEdge(edge) {
  163. const { isAnimated } = this.props;
  164. return (
  165. <EdgeContainer
  166. key={edge.get('id')}
  167. id={edge.get('id')}
  168. source={edge.get('source')}
  169. target={edge.get('target')}
  170. waypoints={edge.get('points')}
  171. highlighted={edge.get('highlighted')}
  172. focused={edge.get('focused')}
  173. scale={edge.get('scale')}
  174. isAnimated={isAnimated}
  175. />
  176. );
  177. }
  178. renderOverlay(element) {
  179. // NOTE: This piece of code is a bit hacky - as we can't set the absolute coords for the
  180. // SVG element, we set the zoom level high enough that we're sure it covers the screen.
  181. const className = classNames('nodes-chart-overlay', { active: element.get('isActive') });
  182. const scale = (this.props.selectedScale || 1) * 100000;
  183. return (
  184. <rect
  185. className={className}
  186. key="nodes-chart-overlay"
  187. transform={`scale(${scale})`}
  188. fill={theme.colors.purple25}
  189. x={-1}
  190. y={-1}
  191. width={2}
  192. height={2}
  193. />
  194. );
  195. }
  196. renderElement(element) {
  197. if (element.get('isOverlay')) {
  198. return this.renderOverlay(element);
  199. }
  200. // This heuristics is not ideal but it works.
  201. return element.get('points') ? this.renderEdge(element) : this.renderNode(element);
  202. }
  203. render() {
  204. const nodes = this.props.layoutNodes.toIndexedSeq()
  205. .map(this.nodeHighlightedDecorator)
  206. .map(this.nodeFocusedDecorator)
  207. .map(this.nodeBlurredDecorator)
  208. .map(this.nodeMatchesDecorator)
  209. .map(this.nodeNetworksDecorator)
  210. .map(this.nodeMetricDecorator)
  211. .map(this.nodeScaleDecorator)
  212. .groupBy(this.nodeDisplayLayer);
  213. const edges = this.props.layoutEdges.toIndexedSeq()
  214. .map(this.edgeHighlightedDecorator)
  215. .map(this.edgeFocusedDecorator)
  216. .map(this.edgeBlurredDecorator)
  217. .map(this.edgeScaleDecorator)
  218. .groupBy(this.edgeDisplayLayer);
  219. // NOTE: The elements need to be arranged into a single array outside
  220. // of DOM structure for React rendering engine to do smart rearrangements
  221. // without unnecessary re-rendering of the elements themselves. So e.g.
  222. // rendering the element layers individually below would be significantly slower.
  223. const orderedElements = makeList([
  224. edges.get(BLURRED_EDGES_LAYER, makeList()),
  225. nodes.get(BLURRED_NODES_LAYER, makeList()),
  226. fromJS([{ isActive: !!nodes.get(BLURRED_NODES_LAYER), isOverlay: true }]),
  227. edges.get(NORMAL_EDGES_LAYER, makeList()),
  228. nodes.get(NORMAL_NODES_LAYER, makeList()),
  229. edges.get(HIGHLIGHTED_EDGES_LAYER, makeList()),
  230. nodes.get(HIGHLIGHTED_NODES_LAYER, makeList()),
  231. edges.get(HOVERED_EDGES_LAYER, makeList()),
  232. nodes.get(HOVERED_NODES_LAYER, makeList()),
  233. ]).flatten(true);
  234. return (
  235. <g className="tour-step-anchor nodes-chart-elements">
  236. {orderedElements.map(this.renderElement)}
  237. </g>
  238. );
  239. }
  240. }
  241. function mapStateToProps(state) {
  242. return {
  243. contrastMode: state.get('contrastMode'),
  244. hasSelectedNode: hasSelectedNodeFn(state),
  245. highlightedEdgeIds: highlightedEdgeIdsSelector(state),
  246. highlightedNodeIds: highlightedNodeIdsSelector(state),
  247. isAnimated: !graphExceedsComplexityThreshSelector(state),
  248. layoutEdges: layoutEdgesSelector(state),
  249. layoutNodes: layoutNodesSelector(state),
  250. mouseOverEdgeId: state.get('mouseOverEdgeId'),
  251. mouseOverNodeId: state.get('mouseOverNodeId'),
  252. neighborsOfSelectedNode: getAdjacentNodes(state),
  253. nodeMetric: nodeMetricSelector(state),
  254. nodeNetworks: nodeNetworksSelector(state),
  255. searchNodeMatches: searchNodeMatchesSelector(state),
  256. searchQuery: state.get('searchQuery'),
  257. selectedNetwork: state.get('selectedNetwork'),
  258. selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state),
  259. selectedNodeId: state.get('selectedNodeId'),
  260. selectedScale: selectedScaleSelector(state),
  261. };
  262. }
  263. export default connect(mapStateToProps)(NodesChartElements);