From 8d329b3f44f3427957d7f488f99d5cda3f93978f Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 17 Mar 2017 16:56:41 +0100 Subject: [PATCH] Polished nodes-resources components. --- .../node-resources-layer-labels-overlay.js | 53 ----------- .../node-resources-layer-topology.js | 33 ++++--- .../nodes-resources/node-resources-layer.js | 23 ++--- .../node-resources-metric-box-info.js | 36 +++++-- .../node-resources-metric-box.js | 95 +++++++++++++++---- client/app/scripts/utils/metric-utils.js | 21 +--- client/app/scripts/utils/transform-utils.js | 13 ++- client/app/styles/_base.scss | 26 ++--- 8 files changed, 161 insertions(+), 139 deletions(-) delete mode 100644 client/app/scripts/components/nodes-resources/node-resources-layer-labels-overlay.js diff --git a/client/app/scripts/components/nodes-resources/node-resources-layer-labels-overlay.js b/client/app/scripts/components/nodes-resources/node-resources-layer-labels-overlay.js deleted file mode 100644 index 3f9d0e1885..0000000000 --- a/client/app/scripts/components/nodes-resources/node-resources-layer-labels-overlay.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { fromJS } from 'immutable'; - -import NodeResourcesMetricBoxInfo from './node-resources-metric-box-info'; -import { applyTransformX, applyTransformY } from '../../utils/transform-utils'; -import { - RESOURCES_LAYER_TITLE_WIDTH, - RESOURCES_LABEL_MIN_SIZE, - RESOURCES_LABEL_PADDING, -} from '../../constants/styles'; - - -export default class LayerLabelsOverlay extends React.Component { - positionedLabels() { - const { verticalOffset, transform, nodes } = this.props; - const y = applyTransformY(transform, verticalOffset); - const labels = []; - - nodes.forEach((node) => { - const xStart = applyTransformX(transform, node.get('offset')); - const xEnd = applyTransformX(transform, node.get('offset') + node.get('width')); - const xTrimmed = Math.max(RESOURCES_LAYER_TITLE_WIDTH, xStart); - const width = xEnd - xTrimmed; - - if (width >= RESOURCES_LABEL_MIN_SIZE) { - labels.push({ - width: width - (2 * RESOURCES_LABEL_PADDING), - x: xTrimmed + RESOURCES_LABEL_PADDING, - y: y + RESOURCES_LABEL_PADDING, - node, - }); - } - }); - - return fromJS(labels); - } - - render() { - return ( - - {this.positionedLabels().map(label => ( - - ))} - - ); - } -} diff --git a/client/app/scripts/components/nodes-resources/node-resources-layer-topology.js b/client/app/scripts/components/nodes-resources/node-resources-layer-topology.js index 62f107c41d..8520703853 100644 --- a/client/app/scripts/components/nodes-resources/node-resources-layer-topology.js +++ b/client/app/scripts/components/nodes-resources/node-resources-layer-topology.js @@ -1,21 +1,30 @@ import React from 'react'; +import pick from 'lodash/pick'; -import { RESOURCES_LAYER_TITLE_WIDTH, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; -import { applyTransformY } from '../../utils/transform-utils'; +import { applyTransform } from '../../utils/transform-utils'; +import { + RESOURCES_LAYER_TITLE_WIDTH, + RESOURCES_LAYER_HEIGHT, +} from '../../constants/styles'; -export default class LayerTopologyName extends React.Component { + +export default class NodeResourcesLayerTopology extends React.Component { render() { - const { verticalOffset, topologyId, transform } = this.props; - const height = RESOURCES_LAYER_HEIGHT * transform.scaleY; - const y = applyTransformY(transform, verticalOffset); + // This component always has a fixed horizontal position and width, + // so we only apply the vertical zooming transformation to match the + // vertical position and height of the resource boxes. + const verticalTransform = pick(this.props.transform, ['translateY', 'scaleY']); + const { width, height, y } = applyTransform(verticalTransform, { + width: RESOURCES_LAYER_TITLE_WIDTH, + height: RESOURCES_LAYER_HEIGHT, + y: this.props.verticalPosition, + }); return ( - - {topologyId} + +
+ {this.props.topologyId} +
); } diff --git a/client/app/scripts/components/nodes-resources/node-resources-layer.js b/client/app/scripts/components/nodes-resources/node-resources-layer.js index 597f59f903..1fa26f3f94 100644 --- a/client/app/scripts/components/nodes-resources/node-resources-layer.js +++ b/client/app/scripts/components/nodes-resources/node-resources-layer.js @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import { Map as makeMap } from 'immutable'; import NodeResourcesMetricBox from './node-resources-metric-box'; -import NodeResourcesLayerLabelsOverlay from './node-resources-layer-labels-overlay'; import NodeResourcesLayerTopology from './node-resources-layer-topology'; import { layersVerticalPositionSelector, @@ -11,38 +10,30 @@ import { } from '../../selectors/resource-view/layers'; -// const stringifiedTransform = ({ scaleX = 1, scaleY = 1, translateX = 0, translateY = 0 }) => ( -// `translate(${translateX},${translateY}) scale(${scaleX},${scaleY})` -// ); - class NodesResourcesLayer extends React.Component { render() { const { layerVerticalPosition, topologyId, transform, nodes } = this.props; return ( - - + + {nodes.toIndexedSeq().map(node => ( ))} - {!nodes.isEmpty() && } @@ -54,7 +45,7 @@ class NodesResourcesLayer extends React.Component { function mapStateToProps(state, props) { return { layerVerticalPosition: layersVerticalPositionSelector(state).get(props.topologyId), - nodes: positionedNodesByTopologySelector(state).get(props.topologyId, makeMap()) + nodes: positionedNodesByTopologySelector(state).get(props.topologyId, makeMap()), }; } diff --git a/client/app/scripts/components/nodes-resources/node-resources-metric-box-info.js b/client/app/scripts/components/nodes-resources/node-resources-metric-box-info.js index 73dfc6c1dc..532e768696 100644 --- a/client/app/scripts/components/nodes-resources/node-resources-metric-box-info.js +++ b/client/app/scripts/components/nodes-resources/node-resources-metric-box-info.js @@ -1,19 +1,37 @@ import React from 'react'; -import { getHumanizedMetricInfo } from '../../utils/metric-utils'; +import { formatMetricSvg } from '../../utils/string-utils'; -const HEIGHT = '45px'; - export default class NodeResourcesMetricBoxInfo extends React.Component { - render() { - const { node, width, x, y } = this.props; - const humanizedMetricInfo = getHumanizedMetricInfo(node.get('activeMetric')); + humanizedMetricInfo() { + const metric = this.props.activeMetric.toJS(); + const showExtendedInfo = metric.withCapacity && metric.format !== 'percent'; + const totalCapacity = formatMetricSvg(metric.totalCapacity, metric); + const absoluteConsumption = formatMetricSvg(metric.absoluteConsumption, metric); + const relativeConsumption = formatMetricSvg(100.0 * metric.relativeConsumption, + { format: 'percent' }); return ( - - {node.get('label')} - {humanizedMetricInfo} + + + {showExtendedInfo ? relativeConsumption : absoluteConsumption} + consumed + {showExtendedInfo && {' - '} + ({absoluteConsumption} / {totalCapacity}) + } + + ); + } + + render() { + const { width, x, y } = this.props; + return ( + +
+ {this.props.label} + {this.humanizedMetricInfo()} +
); } diff --git a/client/app/scripts/components/nodes-resources/node-resources-metric-box.js b/client/app/scripts/components/nodes-resources/node-resources-metric-box.js index cfc665a350..082656bd2f 100644 --- a/client/app/scripts/components/nodes-resources/node-resources-metric-box.js +++ b/client/app/scripts/components/nodes-resources/node-resources-metric-box.js @@ -1,35 +1,97 @@ import React from 'react'; import { connect } from 'react-redux'; +import NodeResourcesMetricBoxInfo from './node-resources-metric-box-info'; +import { applyTransform } from '../../utils/transform-utils'; +import { + RESOURCES_LAYER_TITLE_WIDTH, + RESOURCES_LABEL_MIN_SIZE, + RESOURCES_LABEL_PADDING, +} from '../../constants/styles'; + + +// Transforms the rectangle box according to the zoom state forwarded by +// the zooming wrapper. Two main reasons why we're doing it per component +// instead of on the parent group are: +// 1. Due to single-precision SVG coordinate system implemented by most browsers, +// the resource boxes would be incorrectly rendered on extreme zoom levels (it's +// not just about a few pixels here and there, the whole layout gets screwed). So +// we don't actually use the native SVG transform but transform the coordinates +// ourselves (with `applyTransform` helper). +// 2. That also enables us to do the resources info label clipping, which would otherwise +// not be possible with pure zooming. +// +// The downside is that the rendering becomes slower as the transform prop needs to be forwarded +// down to this component, so a lot of stuff gets rerendered/recalculated on every zoom action. +// On the other hand, this enables us to easily leave out the nodes that are not in the viewport. +const transformedDimensions = (props) => { + const { width, height, x, y } = applyTransform(props.transform, props); + + // Trim the beginning of the resource box just after the layer topology + // name to the left and the viewport width to the right. That enables us + // to make info tags 'sticky', but also not to render the nodes with no + // visible part in the viewport. + const xStart = Math.max(RESOURCES_LAYER_TITLE_WIDTH, x); + const xEnd = Math.min(x + width, props.viewportWidth); + + // Update the horizontal tranform with trimmed values. + return { + width: xEnd - xStart, + height, + x: xStart, + y, + }; +}; class NodeResourcesMetricBox extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = transformedDimensions(props); + } + + componentWillReceiveProps(nextProps) { + this.setState(transformedDimensions(nextProps)); + } + defaultRectProps(relativeHeight = 1) { - const { translateX, translateY, scaleX, scaleY } = this.props.transform; - const innerTranslateY = this.props.height * scaleY * (1 - relativeHeight); - const stroke = this.props.contrastMode ? 'black' : 'white'; + const { x, y, width, height } = this.state; + const translateY = height * (1 - relativeHeight); return { - transform: `translate(0, ${innerTranslateY})`, + transform: `translate(0, ${translateY})`, opacity: this.props.contrastMode ? 1 : 0.85, - height: this.props.height * scaleY * relativeHeight, - width: this.props.width * scaleX, - x: (this.props.x * scaleX) + translateX, - y: (this.props.y * scaleY) + translateY, - vectorEffect: 'non-scaling-stroke', - strokeWidth: 1, - stroke, + stroke: this.props.contrastMode ? 'black' : 'white', + height: height * relativeHeight, + width, + x, + y, }; } render() { - const { color, withCapacity, activeMetric } = this.props; + const { label, color, withCapacity, activeMetric } = this.props; const { relativeConsumption, info } = activeMetric.toJS(); - const frameFill = 'rgba(150, 150, 150, 0.4)'; + const { x, y, width } = this.state; + + const showInfo = width >= RESOURCES_LABEL_MIN_SIZE; + const showNode = width >= 1; + + // Don't display the nodes which are less than 1px wide. + // TODO: Show `+ 31 nodes` kind of tag in their stead. + if (!showNode) return null; return ( - + {info} - {withCapacity && } + {withCapacity && } + {showInfo && } ); } @@ -37,7 +99,8 @@ class NodeResourcesMetricBox extends React.Component { function mapStateToProps(state) { return { - contrastMode: state.get('contrastMode') + contrastMode: state.get('contrastMode'), + viewportWidth: state.getIn(['viewport', 'width']), }; } diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js index 3f201855d1..f9232a5583 100644 --- a/client/app/scripts/utils/metric-utils.js +++ b/client/app/scripts/utils/metric-utils.js @@ -24,7 +24,7 @@ export function renderMetricValue(value, condition) { // loadScale(1) == 0.5; E.g. a nicely balanced system :). const loadScale = scaleLog().domain([0.01, 100]).range([0, 1]); -// Used in the graph view + export function getMetricValue(metric) { if (!metric) { return {height: 0, value: null, formattedValue: 'n/a'}; @@ -54,25 +54,6 @@ export function getMetricValue(metric) { }; } -// Used in the resource view -export function getHumanizedMetricInfo(metric) { - const showExtendedInfo = metric.get('withCapacity') && metric.get('format') !== 'percent'; - const totalCapacity = formatMetricSvg(metric.get('totalCapacity'), metric.toJS()); - const absoluteConsumption = formatMetricSvg(metric.get('absoluteConsumption'), metric.toJS()); - const relativeConsumption = formatMetricSvg(100.0 * metric.get('relativeConsumption'), - { format: 'percent' }); - return ( - - - {showExtendedInfo ? relativeConsumption : absoluteConsumption} - consumed - {showExtendedInfo && {' - '} - ({absoluteConsumption} / {totalCapacity}) - } - - ); -} - export function getMetricColor(metric) { const selectedMetric = metric && metric.get('id'); if (/mem/.test(selectedMetric)) { diff --git a/client/app/scripts/utils/transform-utils.js b/client/app/scripts/utils/transform-utils.js index a4bdfd39f6..4012947e44 100644 --- a/client/app/scripts/utils/transform-utils.js +++ b/client/app/scripts/utils/transform-utils.js @@ -1,3 +1,12 @@ -export const applyTransformX = ({ scaleX = 1, translateX = 0 }, x) => (x * scaleX) + translateX; -export const applyTransformY = ({ scaleY = 1, translateY = 0 }, y) => (y * scaleY) + translateY; +export const applyTranslateX = ({ scaleX = 1, translateX = 0 }, x) => (x * scaleX) + translateX; +export const applyTranslateY = ({ scaleY = 1, translateY = 0 }, y) => (y * scaleY) + translateY; +export const applyScaleX = ({ scaleX = 1 }, width) => width * scaleX; +export const applyScaleY = ({ scaleY = 1 }, height) => height * scaleY; + +export const applyTransform = (transform, { width, height, x, y }) => ({ + x: applyTranslateX(transform, x), + y: applyTranslateY(transform, y), + width: applyScaleX(transform, width), + height: applyScaleY(transform, height), +}); diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 6a5356d300..a0d5430abd 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -941,22 +941,26 @@ } } -.node-resource { - &-info { - background-color: rgba(white, 0.6); - border-radius: 2px; - cursor: default; - padding: 5px; +.node-resources { + &-metric-box { + fill: rgba(150, 150, 150, 0.4); - .wrapper { - display: block; + &-info { + background-color: rgba(white, 0.6); + border-radius: 2px; + cursor: default; + padding: 5px; + + .wrapper { + display: block; - &.label { font-size: 15px; } - &.consumption { font-size: 12px; } + &.label { font-size: 15px; } + &.consumption { font-size: 12px; } + } } } - &-layer .layer-topology-name { + &-layer-topology { background-color: rgba(#eee, 0.95); border: 1px solid #ccc; color: $text-tertiary-color;