node-resources-metric-box.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import React from 'react';
  2. import { connect } from 'react-redux';
  3. import theme from 'weaveworks-ui-components/lib/theme';
  4. import NodeResourcesMetricBoxInfo from './node-resources-metric-box-info';
  5. import { clickNode } from '../../actions/request-actions';
  6. import { trackAnalyticsEvent } from '../../utils/tracking-utils';
  7. import { applyTransform } from '../../utils/transform-utils';
  8. import { RESOURCE_VIEW_MODE } from '../../constants/naming';
  9. import {
  10. RESOURCES_LAYER_TITLE_WIDTH,
  11. RESOURCES_LABEL_MIN_SIZE,
  12. RESOURCES_LABEL_PADDING,
  13. } from '../../constants/styles';
  14. // Transforms the rectangle box according to the zoom state forwarded by
  15. // the zooming wrapper. Two main reasons why we're doing it per component
  16. // instead of on the parent group are:
  17. // 1. Due to single-precision SVG coordinate system implemented by most browsers,
  18. // the resource boxes would be incorrectly rendered on extreme zoom levels (it's
  19. // not just about a few pixels here and there, the whole layout gets screwed). So
  20. // we don't actually use the native SVG transform but transform the coordinates
  21. // ourselves (with `applyTransform` helper).
  22. // 2. That also enables us to do the resources info label clipping, which would otherwise
  23. // not be possible with pure zooming.
  24. //
  25. // The downside is that the rendering becomes slower as the transform prop needs to be forwarded
  26. // down to this component, so a lot of stuff gets rerendered/recalculated on every zoom action.
  27. // On the other hand, this enables us to easily leave out the nodes that are not in the viewport.
  28. const transformedDimensions = (props) => {
  29. const {
  30. width, height, x, y
  31. } = applyTransform(props.transform, props);
  32. // Trim the beginning of the resource box just after the layer topology
  33. // name to the left and the viewport width to the right. That enables us
  34. // to make info tags 'sticky', but also not to render the nodes with no
  35. // visible part in the viewport.
  36. const xStart = Math.max(RESOURCES_LAYER_TITLE_WIDTH, x);
  37. const xEnd = Math.min(x + width, props.viewportWidth);
  38. // Update the horizontal transform with trimmed values.
  39. return {
  40. height,
  41. width: xEnd - xStart,
  42. x: xStart,
  43. y,
  44. };
  45. };
  46. class NodeResourcesMetricBox extends React.Component {
  47. constructor(props, context) {
  48. super(props, context);
  49. this.state = transformedDimensions(props);
  50. this.handleClick = this.handleClick.bind(this);
  51. this.saveNodeRef = this.saveNodeRef.bind(this);
  52. }
  53. componentWillReceiveProps(nextProps) {
  54. this.setState(transformedDimensions(nextProps));
  55. }
  56. handleClick(ev) {
  57. ev.stopPropagation();
  58. trackAnalyticsEvent('scope.node.click', {
  59. layout: RESOURCE_VIEW_MODE,
  60. topologyId: this.props.topologyId,
  61. });
  62. this.props.clickNode(
  63. this.props.id,
  64. this.props.label,
  65. this.nodeRef.getBoundingClientRect(),
  66. this.props.topologyId,
  67. this.props.shape
  68. );
  69. }
  70. saveNodeRef(ref) {
  71. this.nodeRef = ref;
  72. }
  73. defaultRectProps(relativeHeight = 1) {
  74. const {
  75. x, y, width, height
  76. } = this.state;
  77. const translateY = height * (1 - relativeHeight);
  78. return {
  79. height: height * relativeHeight,
  80. opacity: this.props.contrastMode ? 1 : 0.85,
  81. stroke: this.props.contrastMode ? 'black' : 'white',
  82. transform: `translate(0, ${translateY})`,
  83. width,
  84. x,
  85. y,
  86. };
  87. }
  88. render() {
  89. const { x, y, width } = this.state;
  90. const {
  91. id, selectedNodeId, label, color, metricSummary
  92. } = this.props;
  93. const { showCapacity, relativeConsumption, type } = metricSummary.toJS();
  94. const opacity = (selectedNodeId && selectedNodeId !== id) ? 0.35 : 1;
  95. const showInfo = width >= RESOURCES_LABEL_MIN_SIZE;
  96. const showNode = width >= 1; // hide the thin nodes
  97. // Don't display the nodes which are less than 1px wide.
  98. // TODO: Show `+ 31 nodes` kind of tag in their stead.
  99. if (!showNode) return null;
  100. const resourceUsageTooltipInfo = showCapacity
  101. ? metricSummary.get('humanizedRelativeConsumption')
  102. : metricSummary.get('humanizedAbsoluteConsumption');
  103. return (
  104. <g
  105. className="node-resources-metric-box"
  106. style={{ opacity }}
  107. onClick={this.handleClick}
  108. ref={this.saveNodeRef}
  109. >
  110. <title>
  111. {label}
  112. {' '}
  113. -
  114. {' '}
  115. {type}
  116. {' '}
  117. usage at
  118. {' '}
  119. {resourceUsageTooltipInfo}
  120. </title>
  121. {showCapacity && (
  122. <rect
  123. className="frame"
  124. rx={theme.borderRadius.soft}
  125. ry={theme.borderRadius.soft}
  126. {...this.defaultRectProps()}
  127. />
  128. )}
  129. <rect
  130. className="bar"
  131. fill={color}
  132. rx={theme.borderRadius.soft}
  133. ry={theme.borderRadius.soft}
  134. {...this.defaultRectProps(relativeConsumption)}
  135. />
  136. {showInfo && (
  137. <NodeResourcesMetricBoxInfo
  138. label={label}
  139. metricSummary={metricSummary}
  140. width={width - (2 * RESOURCES_LABEL_PADDING)}
  141. x={x + RESOURCES_LABEL_PADDING}
  142. y={y + RESOURCES_LABEL_PADDING}
  143. />
  144. )}
  145. </g>
  146. );
  147. }
  148. }
  149. function mapStateToProps(state) {
  150. return {
  151. contrastMode: state.get('contrastMode'),
  152. selectedNodeId: state.get('selectedNodeId'),
  153. viewportWidth: state.getIn(['viewport', 'width']),
  154. };
  155. }
  156. export default connect(
  157. mapStateToProps,
  158. { clickNode }
  159. )(NodeResourcesMetricBox);