From 08f9691e5341d1479f3d4fa8b3880e52c9fb3d3b Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Thu, 2 Mar 2017 19:33:57 +0100 Subject: [PATCH 01/32] Added resource view selector button --- client/app/scripts/actions/app-actions.js | 10 ++++++++++ client/app/scripts/charts/node.js | 1 + client/app/scripts/components/grid-mode-selector.js | 10 +++++++--- client/app/scripts/constants/action-types.js | 1 + client/app/scripts/reducers/root.js | 4 ++++ client/app/styles/_base.scss | 1 + 6 files changed, 24 insertions(+), 3 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 2199797349..ced7c09a2c 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -267,6 +267,16 @@ export function toggleGridMode(enabledArgument) { }; } +export function toggleResourceView() { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_RESOURCE_VIEW, + selected: !getState().get('resourceView') + }); + updateRoute(getState); + }; +} + export function clickNode(nodeId, label, origin) { return (dispatch, getState) => { dispatch({ diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index 84f3866def..49a943273a 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -97,6 +97,7 @@ class Node extends React.Component { exportingGraph, showingNetworks, stack, id, metric } = this.props; const { hovered } = this.state; + console.log(metric && metric.toJS()); const color = getNodeColor(rank, label, pseudo); const truncate = !focused && !hovered; const labelOffsetY = (showingNetworks && networks) ? 40 : 28; diff --git a/client/app/scripts/components/grid-mode-selector.js b/client/app/scripts/components/grid-mode-selector.js index e8d843674e..24b7b944ad 100644 --- a/client/app/scripts/components/grid-mode-selector.js +++ b/client/app/scripts/components/grid-mode-selector.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import classNames from 'classnames'; -import { toggleGridMode } from '../actions/app-actions'; +import { toggleGridMode, toggleResourceView } from '../actions/app-actions'; const Item = (icons, label, isSelected, onClick) => { @@ -37,7 +37,7 @@ class GridModeSelector extends React.Component { } render() { - const { gridMode } = this.props; + const { gridMode, resourceView } = this.props; return (
@@ -45,6 +45,9 @@ class GridModeSelector extends React.Component { {Item('fa fa-share-alt', 'Graph', !gridMode, this.disableGridMode)} {Item('fa fa-table', 'Table', gridMode, this.enableGridMode)}
+
+ {Item('fa fa-bar-chart', 'Resource view', resourceView, this.props.toggleResourceView)} +
); } @@ -53,10 +56,11 @@ class GridModeSelector extends React.Component { function mapStateToProps(state) { return { gridMode: state.get('gridMode'), + resourceView: state.get('resourceView'), }; } export default connect( mapStateToProps, - { toggleGridMode } + { toggleGridMode, toggleResourceView } )(GridModeSelector); diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index fbf0a45f60..33502e81b0 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -60,6 +60,7 @@ const ACTION_TYPES = [ 'SET_RECEIVED_NODES_DELTA', 'SORT_ORDER_CHANGED', 'SET_GRID_MODE', + 'SET_RESOURCE_VIEW', 'CHANGE_INSTANCE', 'TOGGLE_CONTRAST_MODE', 'SHUTDOWN' diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index b2bf96441a..bb34ea2e69 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -211,6 +211,10 @@ export function rootReducer(state = initialState, action) { return state.setIn(['gridMode'], action.enabled); } + case ActionTypes.SET_RESOURCE_VIEW: { + return state.set('resourceView', action.selected); + } + case ActionTypes.CACHE_ZOOM_STATE: { return state.setIn(activeTopologyZoomCacheKeyPathSelector(state), action.zoomState); } diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 0808d2e0f3..57178bf8d4 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -1199,6 +1199,7 @@ border-radius: $border-radius; border: 1px solid $background-darker-color; display: inline-block; + margin-left: 20px; } &-action { From 7a049c14cd9a55c0eacb12fa5617292148f564d0 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 3 Mar 2017 11:58:12 +0100 Subject: [PATCH 02/32] Showing resource boxes in the resource view --- .../scripts/charts/node-resource-metric.js | 12 +++++++ client/app/scripts/charts/node.js | 2 +- client/app/scripts/charts/nodes-chart.js | 7 ++++- client/app/scripts/charts/resource-view.js | 31 +++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 client/app/scripts/charts/node-resource-metric.js create mode 100644 client/app/scripts/charts/resource-view.js diff --git a/client/app/scripts/charts/node-resource-metric.js b/client/app/scripts/charts/node-resource-metric.js new file mode 100644 index 0000000000..edfa9219ba --- /dev/null +++ b/client/app/scripts/charts/node-resource-metric.js @@ -0,0 +1,12 @@ +import React from 'react'; + +export default class NodeResourceMetric extends React.Component { + render() { + const { index } = this.props; + return ( + + + + ); + } +} diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index 49a943273a..ab69437b94 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -97,7 +97,7 @@ class Node extends React.Component { exportingGraph, showingNetworks, stack, id, metric } = this.props; const { hovered } = this.state; - console.log(metric && metric.toJS()); + // console.log(metric && metric.toJS()); const color = getNodeColor(rank, label, pseudo); const truncate = !focused && !hovered; const labelOffsetY = (showingNetworks && networks) ? 40 : 28; diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 1307a39621..69a7a1002d 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -7,6 +7,7 @@ import { event as d3Event, select } from 'd3-selection'; import { zoom, zoomIdentity } from 'd3-zoom'; import Logo from '../components/logo'; +import ResourceView from './resource-view'; import NodesChartElements from './nodes-chart-elements'; import { clickBackground, cacheZoomState } from '../actions/app-actions'; import { activeLayoutZoomSelector } from '../selectors/nodes-chart-zoom'; @@ -110,7 +111,10 @@ class NodesChart extends React.Component { - + {this.props.resourceView ? + : + + } ); @@ -166,6 +170,7 @@ function mapStateToProps(state) { layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)), selectedNodeId: state.get('selectedNodeId'), forceRelayout: state.get('forceRelayout'), + resourceView: state.get('resourceView'), }; } diff --git a/client/app/scripts/charts/resource-view.js b/client/app/scripts/charts/resource-view.js new file mode 100644 index 0000000000..63a4c08f62 --- /dev/null +++ b/client/app/scripts/charts/resource-view.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import NodeResourceMetric from './node-resource-metric'; + +class ResourceView extends React.Component { + render() { + const { nodes, transform } = this.props; + return ( + + {nodes.toIndexedSeq().map((node, index) => ( + + ))} + + ); + } +} + +function mapStateToProps(state) { + return { + nodes: state.get('nodes'), + }; +} + +export default connect( + mapStateToProps +)(ResourceView); From f0e7007a35af3291494e6b926222e4f245a8bec1 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 3 Mar 2017 13:44:46 +0100 Subject: [PATCH 03/32] Crude CPU resource view prototype --- client/app/scripts/actions/app-actions.js | 3 + .../scripts/charts/node-resource-metric.js | 23 +++- client/app/scripts/charts/resource-view.js | 106 +++++++++++++++++- 3 files changed, 124 insertions(+), 8 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index ced7c09a2c..b4ed77728c 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -274,6 +274,9 @@ export function toggleResourceView() { selected: !getState().get('resourceView') }); updateRoute(getState); + setTimeout(() => { + getAllNodes(getState, dispatch); + }, 1200); }; } diff --git a/client/app/scripts/charts/node-resource-metric.js b/client/app/scripts/charts/node-resource-metric.js index edfa9219ba..9ea5bc938d 100644 --- a/client/app/scripts/charts/node-resource-metric.js +++ b/client/app/scripts/charts/node-resource-metric.js @@ -1,11 +1,28 @@ import React from 'react'; +const frameFill = 'rgba(100, 100, 100, 0.2)'; +const frameStroke = 'rgba(100, 100, 100, 0.5)'; + export default class NodeResourceMetric extends React.Component { + constructor(props, context) { + super(props, context); + this.handleMouseClick = this.handleMouseClick.bind(this); + } + + handleMouseClick() { + console.log(this.props.meta.toJS()); + } + render() { - const { index } = this.props; + const { label, color, width, height, x, y, consumption } = this.props; + const innerHeight = height * consumption; + const transform = 'scale(1,-1)'; + return ( - - + + {label} + + ); } diff --git a/client/app/scripts/charts/resource-view.js b/client/app/scripts/charts/resource-view.js index 63a4c08f62..caa121131e 100644 --- a/client/app/scripts/charts/resource-view.js +++ b/client/app/scripts/charts/resource-view.js @@ -1,18 +1,114 @@ import React from 'react'; import { connect } from 'react-redux'; +import { Map as makeMap } from 'immutable'; +import { getNodeColor } from '../utils/color-utils'; import NodeResourceMetric from './node-resource-metric'; +const basePseudoId = 'base'; + +const layersDefinition = [{ + topologyId: 'hosts', + horizontalPadding: 15, + verticalPadding: 5, + frameHeight: 200, + withCapacity: true, +}, { + topologyId: 'containers', + horizontalPadding: 0.5, + verticalPadding: 5, + frameHeight: 150, + withCapacity: false, +}, { + topologyId: 'processes', + horizontalPadding: 0, + verticalPadding: 5, + frameHeight: 100, + withCapacity: false, +}]; + +const getCPUMetric = node => (node.get('metrics') || makeMap()).find(m => m.get('label') === 'CPU'); + +const processedNodes = (nodesByTopology) => { + const result = []; + const childrenXOffset = { [basePseudoId]: 0 }; + let prevTopologyId = null; + let y = 0; + + layersDefinition.forEach((layerDef, layerIndex) => { + const nodes = nodesByTopology.get(layerDef.topologyId); + if (!nodes) return; + + nodes.forEach((node) => { + const metric = getCPUMetric(node); + if (!metric) return; + + const nodeId = node.get('id'); + const nodeColor = getNodeColor(node.get('rank'), node.get('label'), node.get('pseudo')); + + const totalCapacity = metric.get('max'); + const absoluteConsumption = metric.get('value'); + const relativeConsumption = absoluteConsumption / totalCapacity; + const nodeConsumption = layerDef.withCapacity ? relativeConsumption : 1; + + const nodeWidth = layerDef.withCapacity ? totalCapacity : absoluteConsumption; + const nodeHeight = layerDef.frameHeight; + + const shiftX = nodeWidth + layerDef.horizontalPadding; + const parents = node.get('parents') || makeMap(); + const parent = parents.find(p => p.get('topologyId') === prevTopologyId); + const parentId = parent ? parent.get('id') : basePseudoId; + + const nodeY = y; + const nodeX = childrenXOffset[parentId]; + // NOTE: We don't handle uncontained yet. + if (parentId === basePseudoId && layerIndex > 0) { + return; + } + + childrenXOffset[parentId] += shiftX; + childrenXOffset[nodeId] = nodeX; + + result.push(makeMap({ + id: nodeId, + color: nodeColor, + x: nodeX, + y: nodeY, + width: nodeWidth, + height: nodeHeight, + consumption: nodeConsumption, + withCapacity: layerDef.withCapacity, + label: node.get('label'), + meta: node, + })); + }); + + prevTopologyId = layerDef.topologyId; + y += layerDef.frameHeight + layerDef.verticalPadding; + }); + + return result; +}; + class ResourceView extends React.Component { render() { - const { nodes, transform } = this.props; + const { nodesByTopology, transform } = this.props; + const nodesToRender = processedNodes(nodesByTopology); + return ( - {nodes.toIndexedSeq().map((node, index) => ( + {nodesToRender.map(node => ( ))} @@ -22,7 +118,7 @@ class ResourceView extends React.Component { function mapStateToProps(state) { return { - nodes: state.get('nodes'), + nodesByTopology: state.get('nodesByTopology'), }; } From 688c0f45921c8b657a12004e37ad18dfb8d8cbe7 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 3 Mar 2017 15:41:36 +0100 Subject: [PATCH 04/32] Improved the viewMode state logic --- client/app/scripts/actions/app-actions.js | 26 ++++++++----- client/app/scripts/charts/nodes-chart.js | 10 +++-- client/app/scripts/components/app.js | 13 ++++--- .../scripts/components/grid-mode-selector.js | 39 +++++++------------ client/app/scripts/components/nodes.js | 8 ++-- client/app/scripts/constants/action-types.js | 3 +- client/app/scripts/constants/naming.js | 6 +++ .../scripts/reducers/__tests__/root-test.js | 7 ++-- client/app/scripts/reducers/root.js | 21 +++++----- client/app/scripts/selectors/topology.js | 23 +++++++++++ client/app/scripts/utils/router-utils.js | 2 +- client/app/styles/_base.scss | 4 ++ 12 files changed, 98 insertions(+), 64 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index b4ed77728c..0de2548579 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -24,6 +24,7 @@ import { getCurrentTopologyUrl } from '../utils/topology-utils'; import { storageSet } from '../utils/storage-utils'; import { loadTheme } from '../utils/contrast-utils'; import { activeTopologyOptionsSelector } from '../selectors/topology'; +import { RESOURCE_VIEW_MODE, GRAPH_VIEW_MODE, TABLE_VIEW_MODE } from '../constants/naming'; const log = debug('scope:app-actions'); @@ -254,24 +255,31 @@ export function setViewportDimensions(width, height) { }; } -export function toggleGridMode(enabledArgument) { +export function setGraphView() { return (dispatch, getState) => { - const enabled = (enabledArgument === undefined) ? - !getState().get('gridMode') : - enabledArgument; dispatch({ - type: ActionTypes.SET_GRID_MODE, - enabled + type: ActionTypes.SET_VIEW_MODE, + viewMode: GRAPH_VIEW_MODE, }); updateRoute(getState); }; } -export function toggleResourceView() { +export function setTableView() { return (dispatch, getState) => { dispatch({ - type: ActionTypes.SET_RESOURCE_VIEW, - selected: !getState().get('resourceView') + type: ActionTypes.SET_VIEW_MODE, + viewMode: TABLE_VIEW_MODE, + }); + updateRoute(getState); + }; +} + +export function setResourceView() { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_VIEW_MODE, + viewMode: RESOURCE_VIEW_MODE, }); updateRoute(getState); setTimeout(() => { diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 69a7a1002d..18fddcce6b 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -11,7 +11,11 @@ import ResourceView from './resource-view'; import NodesChartElements from './nodes-chart-elements'; import { clickBackground, cacheZoomState } from '../actions/app-actions'; import { activeLayoutZoomSelector } from '../selectors/nodes-chart-zoom'; -import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/topology'; +import { + activeTopologyZoomCacheKeyPathSelector, + isResourceViewModeSelector +} from '../selectors/topology'; + import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer'; @@ -111,7 +115,7 @@ class NodesChart extends React.Component { - {this.props.resourceView ? + {this.props.isResourceViewMode ? : } @@ -168,9 +172,9 @@ function mapStateToProps(state) { return { layoutZoom: activeLayoutZoomSelector(state), layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)), + isResourceViewMode: isResourceViewModeSelector(state), selectedNodeId: state.get('selectedNodeId'), forceRelayout: state.get('forceRelayout'), - resourceView: state.get('resourceView'), }; } diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 19d7d92454..8955747509 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -21,7 +21,7 @@ import MetricSelector from './metric-selector'; import NetworkSelector from './networks-selector'; import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar'; import { getRouter, getUrlState } from '../utils/router-utils'; -import { activeTopologyOptionsSelector } from '../selectors/topology'; +import { activeTopologyOptionsSelector, isTableViewModeSelector } from '../selectors/topology'; import { availableNetworksSelector } from '../selectors/node-networks'; const BACKSPACE_KEY_CODE = 8; @@ -102,10 +102,11 @@ class App extends React.Component { } render() { - const { gridMode, showingDetails, showingHelp, showingMetricsSelector, + const { isTableViewMode, showingDetails, showingHelp, showingMetricsSelector, showingNetworkSelector, showingTroubleshootingMenu } = this.props; const isIframe = window !== window.top; + // TODO: Remove 'grid', 'topo' constants. return (
{showingDebugToolbar() && } @@ -129,9 +130,9 @@ class App extends React.Component { - - {showingMetricsSelector && !gridMode && } - {showingNetworkSelector && !gridMode && } + + {showingMetricsSelector && !isTableViewMode && } + {showingNetworkSelector && isTableViewMode && } @@ -146,7 +147,7 @@ class App extends React.Component { function mapStateToProps(state) { return { activeTopologyOptions: activeTopologyOptionsSelector(state), - gridMode: state.get('gridMode'), + isTableViewMode: isTableViewModeSelector(state), routeSet: state.get('routeSet'), searchFocused: state.get('searchFocused'), searchQuery: state.get('searchQuery'), diff --git a/client/app/scripts/components/grid-mode-selector.js b/client/app/scripts/components/grid-mode-selector.js index 24b7b944ad..b97fe349de 100644 --- a/client/app/scripts/components/grid-mode-selector.js +++ b/client/app/scripts/components/grid-mode-selector.js @@ -2,8 +2,12 @@ import React from 'react'; import { connect } from 'react-redux'; import classNames from 'classnames'; -import { toggleGridMode, toggleResourceView } from '../actions/app-actions'; - +import { setGraphView, setTableView, setResourceView } from '../actions/app-actions'; +import { + isGraphViewModeSelector, + isTableViewModeSelector, + isResourceViewModeSelector, +} from '../selectors/topology'; const Item = (icons, label, isSelected, onClick) => { const className = classNames('grid-mode-selector-action', { @@ -20,33 +24,17 @@ const Item = (icons, label, isSelected, onClick) => { }; class GridModeSelector extends React.Component { - - constructor(props, context) { - super(props, context); - - this.enableGridMode = this.enableGridMode.bind(this); - this.disableGridMode = this.disableGridMode.bind(this); - } - - enableGridMode() { - return this.props.toggleGridMode(true); - } - - disableGridMode() { - return this.props.toggleGridMode(false); - } - render() { - const { gridMode, resourceView } = this.props; + const { isGraphViewMode, isTableViewMode, isResourceViewMode } = this.props; return (
- {Item('fa fa-share-alt', 'Graph', !gridMode, this.disableGridMode)} - {Item('fa fa-table', 'Table', gridMode, this.enableGridMode)} + {Item('fa fa-share-alt', 'Graph', isGraphViewMode, this.props.setGraphView)} + {Item('fa fa-table', 'Table', isTableViewMode, this.props.setTableView)}
- {Item('fa fa-bar-chart', 'Resource view', resourceView, this.props.toggleResourceView)} + {Item('fa fa-bar-chart', 'Resource view', isResourceViewMode, this.props.setResourceView)}
); @@ -55,12 +43,13 @@ class GridModeSelector extends React.Component { function mapStateToProps(state) { return { - gridMode: state.get('gridMode'), - resourceView: state.get('resourceView'), + isGraphViewMode: isGraphViewModeSelector(state), + isTableViewMode: isTableViewModeSelector(state), + isResourceViewMode: isResourceViewModeSelector(state), }; } export default connect( mapStateToProps, - { toggleGridMode, toggleResourceView } + { setGraphView, setTableView, setResourceView } )(GridModeSelector); diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js index ecca7733ae..fd045ffd79 100644 --- a/client/app/scripts/components/nodes.js +++ b/client/app/scripts/components/nodes.js @@ -9,6 +9,7 @@ import DelayedShow from '../utils/delayed-show'; import { Loading, getNodeType } from './loading'; import { isTopologyEmpty } from '../utils/topology-utils'; import { setViewportDimensions } from '../actions/app-actions'; +import { isTableViewModeSelector } from '../selectors/topology'; import { VIEWPORT_RESIZE_DEBOUNCE_INTERVAL } from '../constants/timer'; @@ -47,9 +48,10 @@ class Nodes extends React.Component { } render() { - const { topologyEmpty, gridMode, topologiesLoaded, nodesLoaded, topologies, + const { topologyEmpty, isTableViewMode, topologiesLoaded, nodesLoaded, topologies, currentTopology } = this.props; + // TODO: Get rid of 'grid'. return (
@@ -60,7 +62,7 @@ class Nodes extends React.Component { {EmptyTopologyError(topologiesLoaded && nodesLoaded && topologyEmpty)} - {gridMode ? : } + {isTableViewMode ? : }
); } @@ -75,8 +77,8 @@ class Nodes extends React.Component { function mapStateToProps(state) { return { + isTableViewMode: isTableViewModeSelector(state), currentTopology: state.get('currentTopology'), - gridMode: state.get('gridMode'), nodesLoaded: state.get('nodesLoaded'), topologies: state.get('topologies'), topologiesLoaded: state.get('topologiesLoaded'), diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index 33502e81b0..a793e0fa83 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -59,8 +59,7 @@ const ACTION_TYPES = [ 'SHOW_NETWORKS', 'SET_RECEIVED_NODES_DELTA', 'SORT_ORDER_CHANGED', - 'SET_GRID_MODE', - 'SET_RESOURCE_VIEW', + 'SET_VIEW_MODE', 'CHANGE_INSTANCE', 'TOGGLE_CONTRAST_MODE', 'SHUTDOWN' diff --git a/client/app/scripts/constants/naming.js b/client/app/scripts/constants/naming.js index d49dc782e0..3b06a61783 100644 --- a/client/app/scripts/constants/naming.js +++ b/client/app/scripts/constants/naming.js @@ -1,2 +1,8 @@ export const EDGE_ID_SEPARATOR = '---'; + +// NOTE: Inconsistent naming is a consequence of +// keeping it backwards-compatible with the old URLs. +export const GRAPH_VIEW_MODE = 'topo'; +export const TABLE_VIEW_MODE = 'grid'; +export const RESOURCE_VIEW_MODE = 'resource'; diff --git a/client/app/scripts/reducers/__tests__/root-test.js b/client/app/scripts/reducers/__tests__/root-test.js index e880d58223..58441f5f28 100644 --- a/client/app/scripts/reducers/__tests__/root-test.js +++ b/client/app/scripts/reducers/__tests__/root-test.js @@ -1,4 +1,5 @@ -import {is, fromJS} from 'immutable'; +import { is, fromJS } from 'immutable'; +import { TABLE_VIEW_MODE } from '../../constants/naming'; // Root reducer test suite using Jasmine matchers import { constructEdgeId } from '../../utils/layouter-utils'; @@ -501,10 +502,10 @@ describe('RootReducer', () => { nextState = reducer(nextState, { type: ActionTypes.CLICK_BACKGROUND }); expect(nextState.get('showingHelp')).toBe(false); }); - it('switches to grid mode when complexity is high', () => { + it('switches to table view when complexity is high', () => { let nextState = initialState.set('currentTopology', fromJS(topologies[0])); nextState = reducer(nextState, {type: ActionTypes.SET_RECEIVED_NODES_DELTA}); - expect(nextState.get('gridMode')).toBe(true); + expect(nextState.get('topologyViewMode')).toEqual(TABLE_VIEW_MODE); expect(nextState.get('initialNodesLoaded')).toBe(true); }); it('cleans up old adjacencies', () => { diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index bb34ea2e69..c8e84d16d6 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -5,7 +5,7 @@ import { fromJS, is as isDeepEqual, List as makeList, Map as makeMap, OrderedMap as makeOrderedMap, Set as makeSet } from 'immutable'; import ActionTypes from '../constants/action-types'; -import { EDGE_ID_SEPARATOR } from '../constants/naming'; +import { EDGE_ID_SEPARATOR, GRAPH_VIEW_MODE, TABLE_VIEW_MODE } from '../constants/naming'; import { graphExceedsComplexityThreshSelector, activeTopologyZoomCacheKeyPathSelector, @@ -42,7 +42,6 @@ export const initialState = makeMap({ errorUrl: null, exportingGraph: false, forceRelayout: false, - gridMode: false, gridSortedBy: null, gridSortedDesc: null, // TODO: Calculate these sets from selectors instead. @@ -78,6 +77,7 @@ export const initialState = makeMap({ topologiesLoaded: false, topologyOptions: makeOrderedMap(), // topologyId -> options topologyUrlsById: makeOrderedMap(), // topologyId -> topologyUrl + topologyViewMode: GRAPH_VIEW_MODE, updatePausedAt: null, // Date version: '...', versionUpdate: null, @@ -207,12 +207,8 @@ export function rootReducer(state = initialState, action) { }); } - case ActionTypes.SET_GRID_MODE: { - return state.setIn(['gridMode'], action.enabled); - } - - case ActionTypes.SET_RESOURCE_VIEW: { - return state.set('resourceView', action.selected); + case ActionTypes.SET_VIEW_MODE: { + return state.set('topologyViewMode', action.viewMode); } case ActionTypes.CACHE_ZOOM_STATE: { @@ -554,9 +550,10 @@ export function rootReducer(state = initialState, action) { // Turn on the table view if the graph is too complex, but skip // this block if the user has already loaded topologies once. if (!state.get('initialNodesLoaded') && !state.get('nodesLoaded')) { - state = graphExceedsComplexityThreshSelector(state) - ? state.set('gridMode', true) - : state; + if (state.get('topologyViewMode') === GRAPH_VIEW_MODE) { + state = graphExceedsComplexityThreshSelector(state) + ? state.set('topologyViewMode', TABLE_VIEW_MODE) : state; + } state = state.set('initialNodesLoaded', true); } return state.set('nodesLoaded', true); @@ -688,7 +685,7 @@ export function rootReducer(state = initialState, action) { selectedNodeId: action.state.selectedNodeId, pinnedMetricType: action.state.pinnedMetricType }); - state = state.set('gridMode', action.state.topologyViewMode === 'grid'); + state = state.set('topologyViewMode', action.state.topologyViewMode); if (action.state.gridSortedBy) { state = state.set('gridSortedBy', action.state.gridSortedBy); } diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index 55dae58eb7..b5a87a0d93 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -1,4 +1,5 @@ import { createSelector } from 'reselect'; +import { RESOURCE_VIEW_MODE, GRAPH_VIEW_MODE, TABLE_VIEW_MODE } from '../constants/naming'; // TODO: Consider moving more stuff from 'topology-utils' here. @@ -31,3 +32,25 @@ export const activeTopologyZoomCacheKeyPathSelector = createSelector( ], (topologyId, topologyOptions) => ['zoomCache', topologyId, JSON.stringify(topologyOptions)] ); + + +export const isGraphViewModeSelector = createSelector( + [ + state => state.get('topologyViewMode'), + ], + viewMode => viewMode === GRAPH_VIEW_MODE +); + +export const isTableViewModeSelector = createSelector( + [ + state => state.get('topologyViewMode'), + ], + viewMode => viewMode === TABLE_VIEW_MODE +); + +export const isResourceViewModeSelector = createSelector( + [ + state => state.get('topologyViewMode'), + ], + viewMode => viewMode === RESOURCE_VIEW_MODE +); diff --git a/client/app/scripts/utils/router-utils.js b/client/app/scripts/utils/router-utils.js index 6bc7d330cb..d3576f0f00 100644 --- a/client/app/scripts/utils/router-utils.js +++ b/client/app/scripts/utils/router-utils.js @@ -48,8 +48,8 @@ export function getUrlState(state) { const urlState = { controlPipe: cp ? cp.toJS() : null, - topologyViewMode: state.get('gridMode') ? 'grid' : 'topo', nodeDetails: nodeDetails.toJS(), + topologyViewMode: state.get('topologyViewMode'), pinnedMetricType: state.get('pinnedMetricType'), pinnedSearches: state.get('pinnedSearches').toJS(), searchQuery: state.get('searchQuery'), diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 57178bf8d4..cbb0bb1e82 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -1253,6 +1253,10 @@ &-selected, &:hover { background-color: $background-darker-secondary-color; } + + &:not(:last-child) { + border-right: 1px solid $background-darker-secondary-color; + } } } From e52b1230b987412efe1c4bde99892ab3e87cc523 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 3 Mar 2017 16:44:19 +0100 Subject: [PATCH 05/32] Extracted zooming into a separate wrapper component --- .../scripts/charts/nodes-chart-elements.js | 4 +- client/app/scripts/charts/nodes-chart.js | 134 ++-------------- client/app/scripts/charts/resource-view.js | 5 +- .../app/scripts/components/zoom-container.js | 143 ++++++++++++++++++ client/app/scripts/selectors/topology.js | 5 +- 5 files changed, 162 insertions(+), 129 deletions(-) create mode 100644 client/app/scripts/components/zoom-container.js diff --git a/client/app/scripts/charts/nodes-chart-elements.js b/client/app/scripts/charts/nodes-chart-elements.js index 44f71e4227..f9cd545400 100644 --- a/client/app/scripts/charts/nodes-chart-elements.js +++ b/client/app/scripts/charts/nodes-chart-elements.js @@ -13,10 +13,10 @@ import { class NodesChartElements extends React.Component { render() { - const { layoutNodes, layoutEdges, selectedScale, transform, isAnimated } = this.props; + const { layoutNodes, layoutEdges, selectedScale, isAnimated } = this.props; return ( - + @@ -115,54 +45,17 @@ class NodesChart extends React.Component { - {this.props.isResourceViewMode ? - : - - } + + {isResourceViewMode ? : } +
); } - cacheZoom() { - const zoomState = pick(this.state, ZOOM_CACHE_FIELDS); - this.props.cacheZoomState(fromJS(zoomState)); - } - - restoreCachedZoom(props) { - if (!props.layoutZoom.isEmpty()) { - const zoomState = props.layoutZoom.toJS(); - - // Restore the zooming settings - this.zoom = this.zoom.scaleExtent([zoomState.minZoomScale, zoomState.maxZoomScale]); - this.svg.call(this.zoom.transform, zoomIdentity - .translate(zoomState.panTranslateX, zoomState.panTranslateY) - .scale(zoomState.zoomScale)); - - // Update the state variables - this.setState(zoomState); - this.zoomRestored = true; - } - } - handleMouseClick() { - if (!this.isZooming || this.props.selectedNodeId) { + if (this.props.selectedNodeId) { this.props.clickBackground(); - } else { - this.isZooming = false; - } - } - - zoomed() { - this.isZooming = true; - // don't pan while node is selected - if (!this.props.selectedNodeId) { - this.setState({ - panTranslateX: d3Event.transform.x, - panTranslateY: d3Event.transform.y, - zoomScale: d3Event.transform.k - }); - this.debouncedCacheZoom(); } } } @@ -170,16 +63,13 @@ class NodesChart extends React.Component { function mapStateToProps(state) { return { - layoutZoom: activeLayoutZoomSelector(state), - layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)), isResourceViewMode: isResourceViewModeSelector(state), selectedNodeId: state.get('selectedNodeId'), - forceRelayout: state.get('forceRelayout'), }; } export default connect( mapStateToProps, - { clickBackground, cacheZoomState } + { clickBackground } )(NodesChart); diff --git a/client/app/scripts/charts/resource-view.js b/client/app/scripts/charts/resource-view.js index caa121131e..a50bc7ce07 100644 --- a/client/app/scripts/charts/resource-view.js +++ b/client/app/scripts/charts/resource-view.js @@ -92,11 +92,10 @@ const processedNodes = (nodesByTopology) => { class ResourceView extends React.Component { render() { - const { nodesByTopology, transform } = this.props; - const nodesToRender = processedNodes(nodesByTopology); + const nodesToRender = processedNodes(this.props.nodesByTopology); return ( - + {nodesToRender.map(node => ( + {this.props.children} + + ); + } + + setZoomTriggers(zoomingEnabled) { + if (zoomingEnabled) { + this.svg.call(this.zoom); + } else { + this.svg.on('.zoom', null); + } + } + + cacheZoom() { + const zoomState = pick(this.state, ZOOM_CACHE_FIELDS); + this.props.cacheZoomState(fromJS(zoomState)); + } + + restoreCachedZoom(props) { + if (!props.layoutZoom.isEmpty()) { + const zoomState = props.layoutZoom.toJS(); + + // Restore the zooming settings + this.zoom = this.zoom.scaleExtent([zoomState.minZoomScale, zoomState.maxZoomScale]); + this.svg.call(this.zoom.transform, zoomIdentity + .translate(zoomState.panTranslateX, zoomState.panTranslateY) + .scale(zoomState.zoomScale)); + + // Update the state variables + this.setState(zoomState); + this.zoomRestored = true; + } + } + + zoomed() { + if (!this.props.disabled) { + this.setState({ + panTranslateX: d3Event.transform.x, + panTranslateY: d3Event.transform.y, + zoomScale: d3Event.transform.k + }); + this.debouncedCacheZoom(); + } + } +} + + +function mapStateToProps(state) { + return { + layoutZoom: activeLayoutZoomSelector(state), + layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)), + forceRelayout: state.get('forceRelayout'), + }; +} + + +export default connect( + mapStateToProps, + { cacheZoomState } +)(ZoomContainer); diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index b5a87a0d93..0990e90380 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -27,10 +27,11 @@ export const activeTopologyOptionsSelector = createSelector( export const activeTopologyZoomCacheKeyPathSelector = createSelector( [ + state => state.get('topologyViewMode'), state => state.get('currentTopologyId'), - activeTopologyOptionsSelector, + state => JSON.stringify(activeTopologyOptionsSelector(state)), ], - (topologyId, topologyOptions) => ['zoomCache', topologyId, JSON.stringify(topologyOptions)] + (viewMode, topologyId, topologyOptions) => ['zoomCache', viewMode, topologyId, topologyOptions] ); From 1e4dd6d5ff1df82909031775ff6925126aaef526 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 6 Mar 2017 16:27:58 +0100 Subject: [PATCH 06/32] Split the layout selectors between graph-view and resource-view --- .../scripts/charts/nodes-chart-elements.js | 2 +- .../app/scripts/components/zoom-container.js | 2 +- .../default-zoom.js} | 33 +++----------- .../graph.js} | 10 ++--- .../layout.js} | 8 ++-- .../viewport.js} | 6 ++- .../selectors/resource-view/default-zoom.js | 45 +++++++++++++++++++ client/app/scripts/selectors/zooming.js | 36 +++++++++++++++ 8 files changed, 104 insertions(+), 38 deletions(-) rename client/app/scripts/selectors/{nodes-chart-zoom.js => graph-view/default-zoom.js} (59%) rename client/app/scripts/selectors/{nodes-chart-graph.js => graph-view/graph.js} (90%) rename client/app/scripts/selectors/{nodes-chart-layout.js => graph-view/layout.js} (96%) rename client/app/scripts/selectors/{canvas-viewport.js => graph-view/viewport.js} (91%) create mode 100644 client/app/scripts/selectors/resource-view/default-zoom.js create mode 100644 client/app/scripts/selectors/zooming.js diff --git a/client/app/scripts/charts/nodes-chart-elements.js b/client/app/scripts/charts/nodes-chart-elements.js index f9cd545400..fb9e1eca4a 100644 --- a/client/app/scripts/charts/nodes-chart-elements.js +++ b/client/app/scripts/charts/nodes-chart-elements.js @@ -8,7 +8,7 @@ import { selectedScaleSelector, layoutNodesSelector, layoutEdgesSelector -} from '../selectors/nodes-chart-layout'; +} from '../selectors/graph-view/layout'; class NodesChartElements extends React.Component { diff --git a/client/app/scripts/components/zoom-container.js b/client/app/scripts/components/zoom-container.js index 5bd205b57b..5433380bfd 100644 --- a/client/app/scripts/components/zoom-container.js +++ b/client/app/scripts/components/zoom-container.js @@ -7,7 +7,7 @@ import { event as d3Event, select } from 'd3-selection'; import { zoom, zoomIdentity } from 'd3-zoom'; import { cacheZoomState } from '../actions/app-actions'; -import { activeLayoutZoomSelector } from '../selectors/nodes-chart-zoom'; +import { activeLayoutZoomSelector } from '../selectors/zooming'; import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/topology'; import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer'; diff --git a/client/app/scripts/selectors/nodes-chart-zoom.js b/client/app/scripts/selectors/graph-view/default-zoom.js similarity index 59% rename from client/app/scripts/selectors/nodes-chart-zoom.js rename to client/app/scripts/selectors/graph-view/default-zoom.js index b80e0cf6b8..3a5d788644 100644 --- a/client/app/scripts/selectors/nodes-chart-zoom.js +++ b/client/app/scripts/selectors/graph-view/default-zoom.js @@ -1,14 +1,13 @@ import { createSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; -import { CANVAS_MARGINS, NODE_BASE_SIZE } from '../constants/styles'; -import { activeTopologyZoomCacheKeyPathSelector } from './topology'; -import { viewportWidthSelector, viewportHeightSelector } from './canvas-viewport'; -import { graphNodesSelector } from './nodes-chart-graph'; +import { CANVAS_MARGINS, NODE_BASE_SIZE } from '../../constants/styles'; +import { viewportWidthSelector, viewportHeightSelector } from './viewport'; +import { graphNodesSelector } from './graph'; -// Compute the default zoom settings for the given graph layout. -const defaultZoomSelector = createSelector( +// Compute the default zoom settings for the given graph. +export const graphDefaultZoomSelector = createSelector( [ graphNodesSelector, viewportWidthSelector, @@ -16,7 +15,7 @@ const defaultZoomSelector = createSelector( ], (graphNodes, width, height) => { if (graphNodes.size === 0) { - return {}; + return makeMap(); } const xMin = graphNodes.minBy(n => n.get('x')).get('x'); @@ -41,24 +40,6 @@ const defaultZoomSelector = createSelector( const panTranslateX = ((width - ((xMax + xMin) * zoomScale)) / 2) + CANVAS_MARGINS.left; const panTranslateY = ((height - ((yMax + yMin) * zoomScale)) / 2) + CANVAS_MARGINS.top; - return { zoomScale, minZoomScale, maxZoomScale, panTranslateX, panTranslateY }; + return makeMap({ zoomScale, minZoomScale, maxZoomScale, panTranslateX, panTranslateY }); } ); - -const activeLayoutCachedZoomSelector = createSelector( - [ - state => state.get('zoomCache'), - activeTopologyZoomCacheKeyPathSelector, - ], - (zoomCache, keyPath) => zoomCache.getIn(keyPath.slice(1)) -); - -// Use the cache to get the last zoom state for the selected topology, -// otherwise use the default zoom options computed from the graph layout. -export const activeLayoutZoomSelector = createSelector( - [ - activeLayoutCachedZoomSelector, - defaultZoomSelector, - ], - (cachedZoomState, defaultZoomState) => makeMap(cachedZoomState || defaultZoomState) -); diff --git a/client/app/scripts/selectors/nodes-chart-graph.js b/client/app/scripts/selectors/graph-view/graph.js similarity index 90% rename from client/app/scripts/selectors/nodes-chart-graph.js rename to client/app/scripts/selectors/graph-view/graph.js index 12aaf635ba..3927048650 100644 --- a/client/app/scripts/selectors/nodes-chart-graph.js +++ b/client/app/scripts/selectors/graph-view/graph.js @@ -2,11 +2,11 @@ import debug from 'debug'; import { createSelector, createStructuredSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; -import { initEdgesFromNodes } from '../utils/layouter-utils'; -import { viewportWidthSelector, viewportHeightSelector } from './canvas-viewport'; -import { activeTopologyOptionsSelector } from './topology'; -import { shownNodesSelector } from './node-filters'; -import { doLayout } from '../charts/nodes-layout'; +import { initEdgesFromNodes } from '../../utils/layouter-utils'; +import { viewportWidthSelector, viewportHeightSelector } from './viewport'; +import { activeTopologyOptionsSelector } from '../topology'; +import { shownNodesSelector } from '../node-filters'; +import { doLayout } from '../../charts/nodes-layout'; import timer from '../utils/timer-utils'; const log = debug('scope:nodes-chart'); diff --git a/client/app/scripts/selectors/nodes-chart-layout.js b/client/app/scripts/selectors/graph-view/layout.js similarity index 96% rename from client/app/scripts/selectors/nodes-chart-layout.js rename to client/app/scripts/selectors/graph-view/layout.js index a908d2ba03..c5b7e2f9e7 100644 --- a/client/app/scripts/selectors/nodes-chart-layout.js +++ b/client/app/scripts/selectors/graph-view/layout.js @@ -3,14 +3,14 @@ import { createSelector } from 'reselect'; import { scaleThreshold } from 'd3-scale'; import { fromJS, Set as makeSet, List as makeList } from 'immutable'; -import { NODE_BASE_SIZE } from '../constants/styles'; -import { graphNodesSelector, graphEdgesSelector } from './nodes-chart-graph'; -import { activeLayoutZoomSelector } from './nodes-chart-zoom'; +import { NODE_BASE_SIZE } from '../../constants/styles'; +import { graphNodesSelector, graphEdgesSelector } from './graph'; +import { activeLayoutZoomSelector } from '../zooming'; import { viewportCircularExpanseSelector, viewportFocusHorizontalCenterSelector, viewportFocusVerticalCenterSelector, -} from './canvas-viewport'; +} from './viewport'; const circularOffsetAngle = Math.PI / 4; diff --git a/client/app/scripts/selectors/canvas-viewport.js b/client/app/scripts/selectors/graph-view/viewport.js similarity index 91% rename from client/app/scripts/selectors/canvas-viewport.js rename to client/app/scripts/selectors/graph-view/viewport.js index c746c7204e..2cf36abb9b 100644 --- a/client/app/scripts/selectors/canvas-viewport.js +++ b/client/app/scripts/selectors/graph-view/viewport.js @@ -1,6 +1,10 @@ import { createSelector } from 'reselect'; -import { CANVAS_MARGINS, DETAILS_PANEL_WIDTH, DETAILS_PANEL_MARGINS } from '../constants/styles'; +import { + CANVAS_MARGINS, + DETAILS_PANEL_WIDTH, + DETAILS_PANEL_MARGINS +} from '../../constants/styles'; export const viewportWidthSelector = createSelector( diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js new file mode 100644 index 0000000000..95bbf913c8 --- /dev/null +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -0,0 +1,45 @@ +// import { createSelector } from 'reselect'; +// import { Map as makeMap } from 'immutable'; +// +// import { CANVAS_MARGINS, NODE_BASE_SIZE } from '../../constants/styles'; +// import { viewportWidthSelector, viewportHeightSelector } from './viewport'; +// import { graphNodesSelector } from './graph'; +// +// +// // Compute the default zoom settings for the given chart. +// export const resourcesDefaultZoomSelector = createSelector( +// [ +// graphNodesSelector, +// viewportWidthSelector, +// viewportHeightSelector, +// ], +// (graphNodes, width, height) => { +// if (graphNodes.size === 0) { +// return {}; +// } +// +// const xMin = graphNodes.minBy(n => n.get('x')).get('x'); +// const xMax = graphNodes.maxBy(n => n.get('x')).get('x'); +// const yMin = graphNodes.minBy(n => n.get('y')).get('y'); +// const yMax = graphNodes.maxBy(n => n.get('y')).get('y'); +// +// const xFactor = width / (xMax - xMin); +// const yFactor = height / (yMax - yMin); +// +// // Maximal allowed zoom will always be such that a node covers 1/5 of the viewport. +// const maxZoomScale = Math.min(width, height) / NODE_BASE_SIZE / 5; +// +// // Initial zoom is such that the graph covers 90% of either the viewport, +// // or one half of maximal zoom constraint, whichever is smaller. +// const zoomScale = Math.min(xFactor, yFactor, maxZoomScale / 2) * 0.9; +// +// // Finally, we always allow zooming out exactly 5x compared to the initial zoom. +// const minZoomScale = zoomScale / 5; +// +// // This translation puts the graph in the center of the viewport, respecting the margins. +// const panTranslateX = ((width - ((xMax + xMin) * zoomScale)) / 2) + CANVAS_MARGINS.left; +// const panTranslateY = ((height - ((yMax + yMin) * zoomScale)) / 2) + CANVAS_MARGINS.top; +// +// return makeMap({ zoomScale, minZoomScale, maxZoomScale, panTranslateX, panTranslateY }); +// } +// ); diff --git a/client/app/scripts/selectors/zooming.js b/client/app/scripts/selectors/zooming.js new file mode 100644 index 0000000000..a51ed7365c --- /dev/null +++ b/client/app/scripts/selectors/zooming.js @@ -0,0 +1,36 @@ +import { createSelector } from 'reselect'; +import { Map as makeMap } from 'immutable'; + +import { graphDefaultZoomSelector } from './graph-view/default-zoom'; +// import { resourceDefaultZoomSelector } from './resource-view/default-zoom'; +import { + activeTopologyZoomCacheKeyPathSelector, + isResourceViewModeSelector, + isGraphViewModeSelector, +} from './topology'; + +const activeLayoutCachedZoomSelector = createSelector( + [ + state => state.get('zoomCache'), + activeTopologyZoomCacheKeyPathSelector, + ], + (zoomCache, keyPath) => zoomCache.getIn(keyPath.slice(1)) +); + +// Use the cache to get the last zoom state for the selected topology, +// otherwise use the default zoom options computed from the layout. +export const activeLayoutZoomSelector = createSelector( + [ + activeLayoutCachedZoomSelector, + isGraphViewModeSelector, + isResourceViewModeSelector, + graphDefaultZoomSelector, + ], + (cachedZoomState, isGraphView, isResourceView, graphDefaultZoom) => { + if (cachedZoomState) return makeMap(cachedZoomState); + // TODO: Modify this. + if (isResourceView) return graphDefaultZoom; + if (isGraphView) return graphDefaultZoom; + return makeMap(); + } +); From a11e3a1de9eb7a820a4c45fae0de7733d96a2b9e Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 6 Mar 2017 18:40:12 +0100 Subject: [PATCH 07/32] Proper zooming logic for the resource view --- .../scripts/charts/node-resource-metric.js | 23 ++++- client/app/scripts/charts/nodes-chart.js | 10 +- client/app/scripts/charts/resource-view.js | 93 +------------------ .../app/scripts/components/zoom-container.js | 51 +++++----- client/app/scripts/constants/styles.js | 21 +++++ .../selectors/graph-view/default-zoom.js | 22 ++--- .../scripts/selectors/graph-view/layout.js | 11 ++- .../selectors/resource-view/default-zoom.js | 82 ++++++++-------- .../scripts/selectors/resource-view/layout.js | 80 ++++++++++++++++ .../selectors/{graph-view => }/viewport.js | 2 +- client/app/scripts/selectors/zooming.js | 8 +- 11 files changed, 213 insertions(+), 190 deletions(-) create mode 100644 client/app/scripts/selectors/resource-view/layout.js rename client/app/scripts/selectors/{graph-view => }/viewport.js (97%) diff --git a/client/app/scripts/charts/node-resource-metric.js b/client/app/scripts/charts/node-resource-metric.js index 9ea5bc938d..4a4820687c 100644 --- a/client/app/scripts/charts/node-resource-metric.js +++ b/client/app/scripts/charts/node-resource-metric.js @@ -16,13 +16,30 @@ export default class NodeResourceMetric extends React.Component { render() { const { label, color, width, height, x, y, consumption } = this.props; const innerHeight = height * consumption; - const transform = 'scale(1,-1)'; + const transform = `translate(${x},${y})`; return ( {label} - - + + ); } diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 5707aac285..c711c8a63d 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -6,7 +6,7 @@ import ZoomContainer from '../components/zoom-container'; import ResourceView from './resource-view'; import NodesChartElements from './nodes-chart-elements'; import { clickBackground } from '../actions/app-actions'; -import { isResourceViewModeSelector } from '../selectors/topology'; +import { isGraphViewModeSelector } from '../selectors/topology'; class NodesChart extends React.Component { @@ -17,7 +17,7 @@ class NodesChart extends React.Component { } render() { - const { isResourceViewMode, isEmpty, selectedNodeId } = this.props; + const { isGraphViewMode, isEmpty, selectedNodeId } = this.props; const markerOffset = selectedNodeId ? '35' : '40'; const markerSize = selectedNodeId ? '10' : '30'; const svgClassNames = isEmpty ? 'hide' : ''; @@ -45,8 +45,8 @@ class NodesChart extends React.Component { - - {isResourceViewMode ? : } + + {isGraphViewMode ? : } @@ -63,7 +63,7 @@ class NodesChart extends React.Component { function mapStateToProps(state) { return { - isResourceViewMode: isResourceViewModeSelector(state), + isGraphViewMode: isGraphViewModeSelector(state), selectedNodeId: state.get('selectedNodeId'), }; } diff --git a/client/app/scripts/charts/resource-view.js b/client/app/scripts/charts/resource-view.js index a50bc7ce07..e1d735299a 100644 --- a/client/app/scripts/charts/resource-view.js +++ b/client/app/scripts/charts/resource-view.js @@ -1,102 +1,15 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Map as makeMap } from 'immutable'; -import { getNodeColor } from '../utils/color-utils'; +import { layoutNodesSelector } from '../selectors/resource-view/layout'; import NodeResourceMetric from './node-resource-metric'; -const basePseudoId = 'base'; - -const layersDefinition = [{ - topologyId: 'hosts', - horizontalPadding: 15, - verticalPadding: 5, - frameHeight: 200, - withCapacity: true, -}, { - topologyId: 'containers', - horizontalPadding: 0.5, - verticalPadding: 5, - frameHeight: 150, - withCapacity: false, -}, { - topologyId: 'processes', - horizontalPadding: 0, - verticalPadding: 5, - frameHeight: 100, - withCapacity: false, -}]; - -const getCPUMetric = node => (node.get('metrics') || makeMap()).find(m => m.get('label') === 'CPU'); - -const processedNodes = (nodesByTopology) => { - const result = []; - const childrenXOffset = { [basePseudoId]: 0 }; - let prevTopologyId = null; - let y = 0; - - layersDefinition.forEach((layerDef, layerIndex) => { - const nodes = nodesByTopology.get(layerDef.topologyId); - if (!nodes) return; - - nodes.forEach((node) => { - const metric = getCPUMetric(node); - if (!metric) return; - - const nodeId = node.get('id'); - const nodeColor = getNodeColor(node.get('rank'), node.get('label'), node.get('pseudo')); - - const totalCapacity = metric.get('max'); - const absoluteConsumption = metric.get('value'); - const relativeConsumption = absoluteConsumption / totalCapacity; - const nodeConsumption = layerDef.withCapacity ? relativeConsumption : 1; - - const nodeWidth = layerDef.withCapacity ? totalCapacity : absoluteConsumption; - const nodeHeight = layerDef.frameHeight; - - const shiftX = nodeWidth + layerDef.horizontalPadding; - const parents = node.get('parents') || makeMap(); - const parent = parents.find(p => p.get('topologyId') === prevTopologyId); - const parentId = parent ? parent.get('id') : basePseudoId; - - const nodeY = y; - const nodeX = childrenXOffset[parentId]; - // NOTE: We don't handle uncontained yet. - if (parentId === basePseudoId && layerIndex > 0) { - return; - } - - childrenXOffset[parentId] += shiftX; - childrenXOffset[nodeId] = nodeX; - - result.push(makeMap({ - id: nodeId, - color: nodeColor, - x: nodeX, - y: nodeY, - width: nodeWidth, - height: nodeHeight, - consumption: nodeConsumption, - withCapacity: layerDef.withCapacity, - label: node.get('label'), - meta: node, - })); - }); - - prevTopologyId = layerDef.topologyId; - y += layerDef.frameHeight + layerDef.verticalPadding; - }); - - return result; -}; class ResourceView extends React.Component { render() { - const nodesToRender = processedNodes(this.props.nodesByTopology); - return ( - {nodesToRender.map(node => ( + {this.props.layoutNodes.map(node => ( @@ -95,8 +88,7 @@ class ZoomContainer extends React.Component { } cacheZoom() { - const zoomState = pick(this.state, ZOOM_CACHE_FIELDS); - this.props.cacheZoomState(fromJS(zoomState)); + this.props.cacheZoomState(fromJS(this.state)); } restoreCachedZoom(props) { @@ -104,10 +96,10 @@ class ZoomContainer extends React.Component { const zoomState = props.layoutZoom.toJS(); // Restore the zooming settings - this.zoom = this.zoom.scaleExtent([zoomState.minZoomScale, zoomState.maxZoomScale]); + this.zoom = this.zoom.scaleExtent([zoomState.minScale, zoomState.maxScale]); this.svg.call(this.zoom.transform, zoomIdentity - .translate(zoomState.panTranslateX, zoomState.panTranslateY) - .scale(zoomState.zoomScale)); + .translate(zoomState.translateX, zoomState.translateY) + .scale(zoomState.scaleX, zoomState.scaleY)); // Update the state variables this.setState(zoomState); @@ -117,11 +109,18 @@ class ZoomContainer extends React.Component { zoomed() { if (!this.props.disabled) { - this.setState({ - panTranslateX: d3Event.transform.x, - panTranslateY: d3Event.transform.y, - zoomScale: d3Event.transform.k - }); + if (this.props.horizontal) { + this.setState({ + scaleX: d3Event.transform.k, + translateX: d3Event.transform.x, + }); + } + if (this.props.vertical) { + this.setState({ + scaleY: d3Event.transform.k, + translateY: d3Event.transform.y, + }); + } this.debouncedCacheZoom(); } } diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index a5f937ad8e..801ff3c16e 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -73,3 +73,24 @@ export const NODE_DETAILS_TABLE_XS_LABEL = { // TODO: consider changing the name of this field on the BE container: '#', }; + +// TODO: Make this variable +export const resourcesLayers = [{ + topologyId: 'hosts', + horizontalPadding: 15, + verticalPadding: 5, + frameHeight: 200, + withCapacity: true, +}, { + topologyId: 'containers', + horizontalPadding: 0.5, + verticalPadding: 5, + frameHeight: 150, + withCapacity: false, +}, { + topologyId: 'processes', + horizontalPadding: 0, + verticalPadding: 5, + frameHeight: 100, + withCapacity: false, +}]; diff --git a/client/app/scripts/selectors/graph-view/default-zoom.js b/client/app/scripts/selectors/graph-view/default-zoom.js index 3a5d788644..8406750103 100644 --- a/client/app/scripts/selectors/graph-view/default-zoom.js +++ b/client/app/scripts/selectors/graph-view/default-zoom.js @@ -2,7 +2,7 @@ import { createSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; import { CANVAS_MARGINS, NODE_BASE_SIZE } from '../../constants/styles'; -import { viewportWidthSelector, viewportHeightSelector } from './viewport'; +import { viewportWidthSelector, viewportHeightSelector } from '../viewport'; import { graphNodesSelector } from './graph'; @@ -18,28 +18,28 @@ export const graphDefaultZoomSelector = createSelector( return makeMap(); } - const xMin = graphNodes.minBy(n => n.get('x')).get('x'); - const xMax = graphNodes.maxBy(n => n.get('x')).get('x'); - const yMin = graphNodes.minBy(n => n.get('y')).get('y'); - const yMax = graphNodes.maxBy(n => n.get('y')).get('y'); + const xMin = graphNodes.map(n => n.get('x') - NODE_BASE_SIZE).min(); + const yMin = graphNodes.map(n => n.get('y') - NODE_BASE_SIZE).min(); + const xMax = graphNodes.map(n => n.get('x') + NODE_BASE_SIZE).max(); + const yMax = graphNodes.map(n => n.get('y') + NODE_BASE_SIZE).max(); const xFactor = width / (xMax - xMin); const yFactor = height / (yMax - yMin); // Maximal allowed zoom will always be such that a node covers 1/5 of the viewport. - const maxZoomScale = Math.min(width, height) / NODE_BASE_SIZE / 5; + const maxScale = Math.min(width, height) / NODE_BASE_SIZE / 5; // Initial zoom is such that the graph covers 90% of either the viewport, // or one half of maximal zoom constraint, whichever is smaller. - const zoomScale = Math.min(xFactor, yFactor, maxZoomScale / 2) * 0.9; + const scale = Math.min(xFactor, yFactor, maxScale / 2) * 0.9; // Finally, we always allow zooming out exactly 5x compared to the initial zoom. - const minZoomScale = zoomScale / 5; + const minScale = scale / 5; // This translation puts the graph in the center of the viewport, respecting the margins. - const panTranslateX = ((width - ((xMax + xMin) * zoomScale)) / 2) + CANVAS_MARGINS.left; - const panTranslateY = ((height - ((yMax + yMin) * zoomScale)) / 2) + CANVAS_MARGINS.top; + const translateX = ((width - ((xMax + xMin) * scale)) / 2) + CANVAS_MARGINS.left; + const translateY = ((height - ((yMax + yMin) * scale)) / 2) + CANVAS_MARGINS.top; - return makeMap({ zoomScale, minZoomScale, maxZoomScale, panTranslateX, panTranslateY }); + return makeMap({ scaleX: scale, scaleY: scale, minScale, maxScale, translateX, translateY }); } ); diff --git a/client/app/scripts/selectors/graph-view/layout.js b/client/app/scripts/selectors/graph-view/layout.js index c5b7e2f9e7..a90a021610 100644 --- a/client/app/scripts/selectors/graph-view/layout.js +++ b/client/app/scripts/selectors/graph-view/layout.js @@ -10,7 +10,7 @@ import { viewportCircularExpanseSelector, viewportFocusHorizontalCenterSelector, viewportFocusVerticalCenterSelector, -} from './viewport'; +} from '../viewport'; const circularOffsetAngle = Math.PI / 4; @@ -28,10 +28,10 @@ const translationToViewportCenterSelector = createSelector( activeLayoutZoomSelector, ], (centerX, centerY, zoomState) => { - const { zoomScale, panTranslateX, panTranslateY } = zoomState.toJS(); + const { scaleX, scaleY, translateX, translateY } = zoomState.toJS(); return { - x: (-panTranslateX + centerX) / zoomScale, - y: (-panTranslateY + centerY) / zoomScale, + x: (-translateX + centerX) / scaleX, + y: (-translateY + centerY) / scaleY, }; } ); @@ -75,7 +75,8 @@ const focusedNodesIdsSelector = createSelector( const circularLayoutScalarsSelector = createSelector( [ - state => activeLayoutZoomSelector(state).get('zoomScale'), + // TODO: Fix this. + state => activeLayoutZoomSelector(state).get('scaleX'), state => focusedNodesIdsSelector(state).length - 1, viewportCircularExpanseSelector, ], diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index 95bbf913c8..65c9b92693 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -1,45 +1,37 @@ -// import { createSelector } from 'reselect'; -// import { Map as makeMap } from 'immutable'; -// -// import { CANVAS_MARGINS, NODE_BASE_SIZE } from '../../constants/styles'; -// import { viewportWidthSelector, viewportHeightSelector } from './viewport'; -// import { graphNodesSelector } from './graph'; -// -// -// // Compute the default zoom settings for the given chart. -// export const resourcesDefaultZoomSelector = createSelector( -// [ -// graphNodesSelector, -// viewportWidthSelector, -// viewportHeightSelector, -// ], -// (graphNodes, width, height) => { -// if (graphNodes.size === 0) { -// return {}; -// } -// -// const xMin = graphNodes.minBy(n => n.get('x')).get('x'); -// const xMax = graphNodes.maxBy(n => n.get('x')).get('x'); -// const yMin = graphNodes.minBy(n => n.get('y')).get('y'); -// const yMax = graphNodes.maxBy(n => n.get('y')).get('y'); -// -// const xFactor = width / (xMax - xMin); -// const yFactor = height / (yMax - yMin); -// -// // Maximal allowed zoom will always be such that a node covers 1/5 of the viewport. -// const maxZoomScale = Math.min(width, height) / NODE_BASE_SIZE / 5; -// -// // Initial zoom is such that the graph covers 90% of either the viewport, -// // or one half of maximal zoom constraint, whichever is smaller. -// const zoomScale = Math.min(xFactor, yFactor, maxZoomScale / 2) * 0.9; -// -// // Finally, we always allow zooming out exactly 5x compared to the initial zoom. -// const minZoomScale = zoomScale / 5; -// -// // This translation puts the graph in the center of the viewport, respecting the margins. -// const panTranslateX = ((width - ((xMax + xMin) * zoomScale)) / 2) + CANVAS_MARGINS.left; -// const panTranslateY = ((height - ((yMax + yMin) * zoomScale)) / 2) + CANVAS_MARGINS.top; -// -// return makeMap({ zoomScale, minZoomScale, maxZoomScale, panTranslateX, panTranslateY }); -// } -// ); +import { createSelector } from 'reselect'; +import { Map as makeMap } from 'immutable'; + +import { CANVAS_MARGINS } from '../../constants/styles'; +import { viewportWidthSelector, viewportHeightSelector } from '../viewport'; +import { layoutNodesSelector } from './layout'; + + +// Compute the default zoom settings for the given chart. +export const resourcesDefaultZoomSelector = createSelector( + [ + layoutNodesSelector, + viewportWidthSelector, + viewportHeightSelector, + ], + (layoutNodes, width, height) => { + if (layoutNodes.size === 0) { + return makeMap(); + } + + const xMin = layoutNodes.map(n => n.get('x')).min(); + const yMin = layoutNodes.map(n => n.get('y')).min(); + const xMax = layoutNodes.map(n => n.get('x') + n.get('width')).max(); + const yMax = layoutNodes.map(n => n.get('y') + n.get('height')).max(); + + const scaleX = (width / (xMax - xMin)) * 0.9; + const scaleY = (height / (yMax - yMin)) * 0.9; + const minScale = scaleX * 0.5; + const maxScale = scaleX * 1000; + + // This translation puts the graph in the center of the viewport, respecting the margins. + const translateX = ((width - ((xMax + xMin) * scaleX)) / 2) + CANVAS_MARGINS.left; + const translateY = ((height - ((yMax + yMin) * scaleY)) / 2) + CANVAS_MARGINS.top; + + return makeMap({ scaleX, scaleY, minScale, maxScale, translateX, translateY }); + } +); diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js new file mode 100644 index 0000000000..fc9e489515 --- /dev/null +++ b/client/app/scripts/selectors/resource-view/layout.js @@ -0,0 +1,80 @@ +import { createSelector } from 'reselect'; +// import { createMapSelector } from 'reselect-map'; +import { fromJS, Map as makeMap } from 'immutable'; + +import { resourcesLayers } from '../../constants/styles'; +import { getNodeColor } from '../../utils/color-utils'; + + +const basePseudoId = 'base'; + +// TODO: Make this variable +const getCPUMetric = node => (node.get('metrics') || makeMap()).find(m => m.get('label') === 'CPU'); + +// TODO: Parse this logic into multiple smarter selectors +export const layoutNodesSelector = createSelector( + [ + state => state.get('nodesByTopology'), + ], + (nodesByTopology) => { + const result = []; + const childrenXOffset = { [basePseudoId]: 0 }; + let prevTopologyId = null; + let y = 0; + + resourcesLayers.forEach((layerDef, layerIndex) => { + y -= layerDef.frameHeight + layerDef.verticalPadding; + + const nodes = nodesByTopology.get(layerDef.topologyId); + if (!nodes) return; + + nodes.forEach((node) => { + const metric = getCPUMetric(node); + if (!metric) return; + + const nodeId = node.get('id'); + const nodeColor = getNodeColor(node.get('rank'), node.get('label'), node.get('pseudo')); + + const totalCapacity = metric.get('max'); + const absoluteConsumption = metric.get('value'); + const relativeConsumption = absoluteConsumption / totalCapacity; + const nodeConsumption = layerDef.withCapacity ? relativeConsumption : 1; + + const nodeWidth = layerDef.withCapacity ? totalCapacity : absoluteConsumption; + const nodeHeight = layerDef.frameHeight; + + const shiftX = nodeWidth + layerDef.horizontalPadding; + const parents = node.get('parents') || makeMap(); + const parent = parents.find(p => p.get('topologyId') === prevTopologyId); + const parentId = parent ? parent.get('id') : basePseudoId; + + const nodeY = y; + const nodeX = childrenXOffset[parentId]; + // NOTE: We don't handle uncontained yet. + if (parentId === basePseudoId && layerIndex > 0) { + return; + } + + childrenXOffset[parentId] += shiftX; + childrenXOffset[nodeId] = nodeX; + + result.push(makeMap({ + id: nodeId, + color: nodeColor, + x: nodeX, + y: nodeY, + width: nodeWidth, + height: nodeHeight, + consumption: nodeConsumption, + withCapacity: layerDef.withCapacity, + label: node.get('label'), + meta: node, + })); + }); + + prevTopologyId = layerDef.topologyId; + }); + + return fromJS(result); + } +); diff --git a/client/app/scripts/selectors/graph-view/viewport.js b/client/app/scripts/selectors/viewport.js similarity index 97% rename from client/app/scripts/selectors/graph-view/viewport.js rename to client/app/scripts/selectors/viewport.js index 2cf36abb9b..0f097c3b18 100644 --- a/client/app/scripts/selectors/graph-view/viewport.js +++ b/client/app/scripts/selectors/viewport.js @@ -4,7 +4,7 @@ import { CANVAS_MARGINS, DETAILS_PANEL_WIDTH, DETAILS_PANEL_MARGINS -} from '../../constants/styles'; +} from '../constants/styles'; export const viewportWidthSelector = createSelector( diff --git a/client/app/scripts/selectors/zooming.js b/client/app/scripts/selectors/zooming.js index a51ed7365c..99bda3d3d0 100644 --- a/client/app/scripts/selectors/zooming.js +++ b/client/app/scripts/selectors/zooming.js @@ -2,7 +2,7 @@ import { createSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; import { graphDefaultZoomSelector } from './graph-view/default-zoom'; -// import { resourceDefaultZoomSelector } from './resource-view/default-zoom'; +import { resourcesDefaultZoomSelector } from './resource-view/default-zoom'; import { activeTopologyZoomCacheKeyPathSelector, isResourceViewModeSelector, @@ -25,11 +25,11 @@ export const activeLayoutZoomSelector = createSelector( isGraphViewModeSelector, isResourceViewModeSelector, graphDefaultZoomSelector, + resourcesDefaultZoomSelector, ], - (cachedZoomState, isGraphView, isResourceView, graphDefaultZoom) => { + (cachedZoomState, isGraphView, isResourceView, graphDefaultZoom, resourcesDefaultZoom) => { if (cachedZoomState) return makeMap(cachedZoomState); - // TODO: Modify this. - if (isResourceView) return graphDefaultZoom; + if (isResourceView) return resourcesDefaultZoom; if (isGraphView) return graphDefaultZoom; return makeMap(); } From 4d368061474af3a3edd45d42a91351fe57cf7435 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 7 Mar 2017 15:16:07 +0100 Subject: [PATCH 08/32] Moved all node networks utils to selectors --- client/app/scripts/components/app.js | 12 ++++-- .../app/scripts/components/metric-selector.js | 12 ++---- client/app/scripts/reducers/root.js | 3 -- client/app/scripts/selectors/node-networks.js | 39 ++++++++++--------- .../app/scripts/utils/network-view-utils.js | 15 ------- 5 files changed, 33 insertions(+), 48 deletions(-) delete mode 100644 client/app/scripts/utils/network-view-utils.js diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 8955747509..c6ba66a051 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -21,8 +21,13 @@ import MetricSelector from './metric-selector'; import NetworkSelector from './networks-selector'; import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar'; import { getRouter, getUrlState } from '../utils/router-utils'; -import { activeTopologyOptionsSelector, isTableViewModeSelector } from '../selectors/topology'; import { availableNetworksSelector } from '../selectors/node-networks'; +import { + activeTopologyOptionsSelector, + isTableViewModeSelector, + isGraphViewModeSelector, +} from '../selectors/topology'; + const BACKSPACE_KEY_CODE = 8; const ENTER_KEY_CODE = 13; @@ -102,7 +107,7 @@ class App extends React.Component { } render() { - const { isTableViewMode, showingDetails, showingHelp, showingMetricsSelector, + const { isTableViewMode, isGraphViewMode, showingDetails, showingHelp, showingMetricsSelector, showingNetworkSelector, showingTroubleshootingMenu } = this.props; const isIframe = window !== window.top; @@ -132,7 +137,7 @@ class App extends React.Component { {showingMetricsSelector && !isTableViewMode && } - {showingNetworkSelector && isTableViewMode && } + {showingNetworkSelector && isGraphViewMode && } @@ -148,6 +153,7 @@ function mapStateToProps(state) { return { activeTopologyOptions: activeTopologyOptionsSelector(state), isTableViewMode: isTableViewModeSelector(state), + isGraphViewMode: isGraphViewModeSelector(state), routeSet: state.get('routeSet'), searchFocused: state.get('searchFocused'), searchQuery: state.get('searchQuery'), diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index daa2f54149..619a52f2c7 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -5,9 +5,9 @@ import { selectMetric } from '../actions/app-actions'; import MetricSelectorItem from './metric-selector-item'; class MetricSelector extends React.Component { - constructor(props, context) { super(props, context); + this.onMouseOut = this.onMouseOut.bind(this); } @@ -16,16 +16,12 @@ class MetricSelector extends React.Component { } render() { - const {availableCanvasMetrics} = this.props; - - const items = availableCanvasMetrics.map(metric => ( - - )); - return (
- {items} + {this.props.availableCanvasMetrics.map(metric => ( + + ))}
); diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index c8e84d16d6..4f011bb266 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -11,7 +11,6 @@ import { activeTopologyZoomCacheKeyPathSelector, } from '../selectors/topology'; import { applyPinnedSearches } from '../utils/search-utils'; -import { getNetworkNodes } from '../utils/network-view-utils'; import { findTopologyById, getAdjacentNodes, @@ -51,7 +50,6 @@ export const initialState = makeMap({ initialNodesLoaded: false, mouseOverEdgeId: null, mouseOverNodeId: null, - networkNodes: makeMap(), nodeDetails: makeOrderedMap(), // nodeId -> details nodes: makeOrderedMap(), // nodeId -> node nodesLoaded: false, @@ -601,7 +599,6 @@ export function rootReducer(state = initialState, action) { // apply pinned searches, filters nodes that dont match state = applyPinnedSearches(state); - state = state.set('networkNodes', getNetworkNodes(state)); state = state.set('availableCanvasMetrics', state.get('nodes') .valueSeq() diff --git a/client/app/scripts/selectors/node-networks.js b/client/app/scripts/selectors/node-networks.js index 830c2b98f8..0654cd10df 100644 --- a/client/app/scripts/selectors/node-networks.js +++ b/client/app/scripts/selectors/node-networks.js @@ -1,28 +1,22 @@ import { createSelector } from 'reselect'; import { createMapSelector } from 'reselect-map'; -import { fromJS, Map as makeMap, List as makeList } from 'immutable'; +import { fromJS, List as makeList, Map as makeMap } from 'immutable'; -const extractNodeNetworksValue = (node) => { - if (node.has('metadata')) { - const networks = node.get('metadata') - .find(field => field.get('id') === 'docker_container_networks'); - return networks && networks.get('value'); - } - return null; -}; +const NETWORKS_ID = 'docker_container_networks'; // TODO: Move this setting of networks as toplevel node field to backend, // to not rely on field IDs here. should be determined by topology implementer. export const nodeNetworksSelector = createMapSelector( [ - state => state.get('nodes').map(extractNodeNetworksValue), + state => state.get('nodes'), ], - (networksValue) => { - if (!networksValue) { - return makeList(); - } - return fromJS(networksValue.split(', ').map(network => ({ + (node) => { + const metadata = node.get('metadata', makeList()); + const networks = metadata.find(f => f.get('id') === NETWORKS_ID) || makeMap(); + const networkValues = networks.has('value') ? networks.get('value').split(', ') : []; + + return fromJS(networkValues.map(network => ({ id: network, label: network, colorKey: network }))); } @@ -36,12 +30,19 @@ export const availableNetworksSelector = createSelector( .sortBy(m => m.get('label')) ); -// NOTE: Don't use this selector directly in mapStateToProps -// as it would get called too many times. export const selectedNetworkNodesIdsSelector = createSelector( [ - state => state.get('networkNodes'), + nodeNetworksSelector, state => state.get('selectedNetwork'), ], - (networkNodes, selectedNetwork) => networkNodes.get(selectedNetwork, makeMap()) + (nodeNetworks, selectedNetworkId) => { + const nodeIds = []; + nodeNetworks.forEach((networks, nodeId) => { + const networksIds = networks.map(n => n.get('id')); + if (networksIds.contains(selectedNetworkId)) { + nodeIds.push(nodeId); + } + }); + return fromJS(nodeIds); + } ); diff --git a/client/app/scripts/utils/network-view-utils.js b/client/app/scripts/utils/network-view-utils.js deleted file mode 100644 index 08a66ceb61..0000000000 --- a/client/app/scripts/utils/network-view-utils.js +++ /dev/null @@ -1,15 +0,0 @@ -import { fromJS } from 'immutable'; - -import { nodeNetworksSelector } from '../selectors/node-networks'; - -export function getNetworkNodes(state) { - const networksMap = {}; - nodeNetworksSelector(state).forEach((networks, nodeId) => { - networks.forEach((network) => { - const networkId = network.get('id'); - networksMap[networkId] = networksMap[networkId] || []; - networksMap[networkId].push(nodeId); - }); - }); - return fromJS(networksMap); -} From b546acfa3181e5237f54fa1865361463575b3424 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 8 Mar 2017 13:23:21 +0100 Subject: [PATCH 09/32] Improved the zoom caching logic --- client/app/scripts/actions/app-actions.js | 4 ++ .../scripts/charts/node-resource-metric.js | 2 +- client/app/scripts/charts/nodes-chart.js | 6 +-- ...-container.js => cachable-zoom-wrapper.js} | 39 +++++++++++-------- client/app/scripts/constants/styles.js | 2 +- .../scripts/selectors/resource-view/layout.js | 8 ++-- client/app/scripts/selectors/zooming.js | 20 +++------- 7 files changed, 43 insertions(+), 38 deletions(-) rename client/app/scripts/components/{zoom-container.js => cachable-zoom-wrapper.js} (81%) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 0de2548579..602569f2c7 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -721,6 +721,10 @@ export function route(urlState) { state.get('nodeDetails'), dispatch ); + // TODO: Remove this + setTimeout(() => { + getAllNodes(getState, dispatch); + }, 1200); }; } diff --git a/client/app/scripts/charts/node-resource-metric.js b/client/app/scripts/charts/node-resource-metric.js index 4a4820687c..db59dcffb3 100644 --- a/client/app/scripts/charts/node-resource-metric.js +++ b/client/app/scripts/charts/node-resource-metric.js @@ -19,7 +19,7 @@ export default class NodeResourceMetric extends React.Component { const transform = `translate(${x},${y})`; return ( - + {label} - + {isGraphViewMode ? : } - + ); diff --git a/client/app/scripts/components/zoom-container.js b/client/app/scripts/components/cachable-zoom-wrapper.js similarity index 81% rename from client/app/scripts/components/zoom-container.js rename to client/app/scripts/components/cachable-zoom-wrapper.js index 6fdbd82eac..72093fba8d 100644 --- a/client/app/scripts/components/zoom-container.js +++ b/client/app/scripts/components/cachable-zoom-wrapper.js @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { debounce } from 'lodash'; +import { debounce, pick } from 'lodash'; import { fromJS } from 'immutable'; import { event as d3Event, select } from 'd3-selection'; @@ -13,7 +13,7 @@ import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/topology'; import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer'; -class ZoomContainer extends React.Component { +class CachableZoomWrapper extends React.Component { constructor(props, context) { super(props, context); @@ -87,8 +87,19 @@ class ZoomContainer extends React.Component { } } + cachableState(state = this.state) { + let cachableFields = ['minScale', 'maxScale']; + if (!this.props.fixHorizontal) { + cachableFields = cachableFields.concat(['scaleX', 'translateX']); + } + if (!this.props.fixVertical) { + cachableFields = cachableFields.concat(['scaleY', 'translateY']); + } + return pick(state, cachableFields); + } + cacheZoom() { - this.props.cacheZoomState(fromJS(this.state)); + this.props.cacheZoomState(fromJS(this.cachableState())); } restoreCachedZoom(props) { @@ -109,18 +120,14 @@ class ZoomContainer extends React.Component { zoomed() { if (!this.props.disabled) { - if (this.props.horizontal) { - this.setState({ - scaleX: d3Event.transform.k, - translateX: d3Event.transform.x, - }); - } - if (this.props.vertical) { - this.setState({ - scaleY: d3Event.transform.k, - translateY: d3Event.transform.y, - }); - } + const updatedState = { + scaleX: d3Event.transform.k, + scaleY: d3Event.transform.k, + translateX: d3Event.transform.x, + translateY: d3Event.transform.y, + }; + + this.setState(this.cachableState(updatedState)); this.debouncedCacheZoom(); } } @@ -139,4 +146,4 @@ function mapStateToProps(state) { export default connect( mapStateToProps, { cacheZoomState } -)(ZoomContainer); +)(CachableZoomWrapper); diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index 801ff3c16e..e2ea219d09 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -83,7 +83,7 @@ export const resourcesLayers = [{ withCapacity: true, }, { topologyId: 'containers', - horizontalPadding: 0.5, + horizontalPadding: 0, verticalPadding: 5, frameHeight: 150, withCapacity: false, diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js index fc9e489515..86d8bf465e 100644 --- a/client/app/scripts/selectors/resource-view/layout.js +++ b/client/app/scripts/selectors/resource-view/layout.js @@ -51,9 +51,11 @@ export const layoutNodesSelector = createSelector( const nodeY = y; const nodeX = childrenXOffset[parentId]; // NOTE: We don't handle uncontained yet. - if (parentId === basePseudoId && layerIndex > 0) { - return; - } + if (parentId === basePseudoId && layerIndex > 0) return; + + // console.log(nodeX, parentId); + // TODO: Remove. + if (nodeX === undefined) return; childrenXOffset[parentId] += shiftX; childrenXOffset[nodeId] = nodeX; diff --git a/client/app/scripts/selectors/zooming.js b/client/app/scripts/selectors/zooming.js index 99bda3d3d0..3b782fef03 100644 --- a/client/app/scripts/selectors/zooming.js +++ b/client/app/scripts/selectors/zooming.js @@ -3,34 +3,26 @@ import { Map as makeMap } from 'immutable'; import { graphDefaultZoomSelector } from './graph-view/default-zoom'; import { resourcesDefaultZoomSelector } from './resource-view/default-zoom'; -import { - activeTopologyZoomCacheKeyPathSelector, - isResourceViewModeSelector, - isGraphViewModeSelector, -} from './topology'; +import { activeTopologyZoomCacheKeyPathSelector, isGraphViewModeSelector } from './topology'; + const activeLayoutCachedZoomSelector = createSelector( [ state => state.get('zoomCache'), activeTopologyZoomCacheKeyPathSelector, ], - (zoomCache, keyPath) => zoomCache.getIn(keyPath.slice(1)) + (zoomCache, keyPath) => zoomCache.getIn(keyPath.slice(1), makeMap()) ); -// Use the cache to get the last zoom state for the selected topology, -// otherwise use the default zoom options computed from the layout. export const activeLayoutZoomSelector = createSelector( [ activeLayoutCachedZoomSelector, isGraphViewModeSelector, - isResourceViewModeSelector, graphDefaultZoomSelector, resourcesDefaultZoomSelector, ], - (cachedZoomState, isGraphView, isResourceView, graphDefaultZoom, resourcesDefaultZoom) => { - if (cachedZoomState) return makeMap(cachedZoomState); - if (isResourceView) return resourcesDefaultZoom; - if (isGraphView) return graphDefaultZoom; - return makeMap(); + (cachedZoomState, isGraphView, graphDefaultZoom, resourcesDefaultZoom) => { + const defaultZoom = isGraphView ? graphDefaultZoom : resourcesDefaultZoom; + return defaultZoom.merge(cachedZoomState); } ); From af0881d96295f2912b5fe5b4aa79e810470a202c Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 10 Mar 2017 02:16:50 +0100 Subject: [PATCH 10/32] Further refactoring of selectors --- .../scripts/charts/node-resource-metric.js | 7 +- client/app/scripts/constants/styles.js | 27 ++++- .../selectors/resource-view/layer-factory.js | 76 ++++++++++++++ .../scripts/selectors/resource-view/layout.js | 98 +++++-------------- 4 files changed, 131 insertions(+), 77 deletions(-) create mode 100644 client/app/scripts/selectors/resource-view/layer-factory.js diff --git a/client/app/scripts/charts/node-resource-metric.js b/client/app/scripts/charts/node-resource-metric.js index db59dcffb3..09ed93c8d7 100644 --- a/client/app/scripts/charts/node-resource-metric.js +++ b/client/app/scripts/charts/node-resource-metric.js @@ -1,7 +1,8 @@ import React from 'react'; const frameFill = 'rgba(100, 100, 100, 0.2)'; -const frameStroke = 'rgba(100, 100, 100, 0.5)'; +const frameStroke = 'rgba(255, 255, 255, 1)'; +const frameStrokeWidth = 1.5; export default class NodeResourceMetric extends React.Component { constructor(props, context) { @@ -25,7 +26,7 @@ export default class NodeResourceMetric extends React.Component { className="wrapper" fill={frameFill} stroke={frameStroke} - strokeWidth="1" + strokeWidth={frameStrokeWidth} vectorEffect="non-scaling-stroke" height={height} width={width} @@ -34,7 +35,7 @@ export default class NodeResourceMetric extends React.Component { className="bar" fill={color} stroke={frameStroke} - strokeWidth="1" + strokeWidth={frameStrokeWidth} vectorEffect="non-scaling-stroke" y={height - innerHeight} height={innerHeight} diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index e2ea219d09..56c8dfdc13 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -77,14 +77,14 @@ export const NODE_DETAILS_TABLE_XS_LABEL = { // TODO: Make this variable export const resourcesLayers = [{ topologyId: 'hosts', - horizontalPadding: 15, - verticalPadding: 5, + horizontalPadding: 0, + verticalPadding: 10, frameHeight: 200, withCapacity: true, }, { topologyId: 'containers', horizontalPadding: 0, - verticalPadding: 5, + verticalPadding: 7.5, frameHeight: 150, withCapacity: false, }, { @@ -94,3 +94,24 @@ export const resourcesLayers = [{ frameHeight: 100, withCapacity: false, }]; + +export const layersDefs = { + hosts: { + parentTopologyId: null, + verticalPadding: 10, + frameHeight: 200, + withCapacity: true, + }, + containers: { + parentTopologyId: 'hosts', + verticalPadding: 7.5, + frameHeight: 150, + withCapacity: false, + }, + processes: { + parentTopologyId: 'containers', + verticalPadding: 5, + frameHeight: 100, + withCapacity: false, + }, +}; diff --git a/client/app/scripts/selectors/resource-view/layer-factory.js b/client/app/scripts/selectors/resource-view/layer-factory.js new file mode 100644 index 0000000000..24cd2af810 --- /dev/null +++ b/client/app/scripts/selectors/resource-view/layer-factory.js @@ -0,0 +1,76 @@ +import { createSelector, createStructuredSelector } from 'reselect'; +import { fromJS, Map as makeMap } from 'immutable'; + +import { layersDefs } from '../../constants/styles'; +import { getNodeColor } from '../../utils/color-utils'; +/* eslint no-unused-vars: 0 */ +/* eslint no-nested-ternary: 0 */ +/* eslint no-sequences: 0 */ + +const basePseudoId = 'base'; + +// TODO: Make this variable +const getCPUMetric = node => (node.get('metrics') || makeMap()).find(m => m.get('label') === 'CPU'); + +export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) => ( + createSelector( + [ + state => state.getIn(['nodesByTopology', topologyId], makeMap()), + parentLayerNodesSelector, + ], + (nodes, parentLayerNodes) => { + const childrenXOffset = { [basePseudoId]: 0 }; + const layerDef = layersDefs[topologyId]; + let positionedNodes = makeMap(); + + parentLayerNodes = parentLayerNodes || makeMap({ basePseudoId: makeMap({ x: 0 }) }); + + nodes.forEach((node) => { + const metric = getCPUMetric(node); + if (!metric) return; + + const nodeId = node.get('id'); + const nodeColor = getNodeColor(node.get('rank'), node.get('label'), node.get('pseudo')); + + const totalCapacity = metric.get('max'); + const absoluteConsumption = metric.get('value'); + const relativeConsumption = absoluteConsumption / totalCapacity; + const nodeConsumption = layerDef.withCapacity ? relativeConsumption : 1; + + const nodeWidth = layerDef.withCapacity ? totalCapacity : absoluteConsumption; + const nodeHeight = layerDef.frameHeight; + + const parents = node.get('parents') || makeMap(); + const parent = parents.find(p => p.get('topologyId') === layerDef.parentTopologyId); + const parentId = parent ? parent.get('id') : basePseudoId; + + // NOTE: We don't handle uncontained yet. + if (parentId === basePseudoId && topologyId !== 'hosts') return; + + childrenXOffset[parentId] = childrenXOffset[parentId] + || parentLayerNodes.getIn([parentId, 'x'], 0); + const nodeX = childrenXOffset[parentId]; + const nodeY = topologyId === 'hosts' ? 0 : (topologyId === 'containers' ? -160 : -265); + + // console.log(nodeX, parentId); + // TODO: Remove. + if (nodeX === undefined) return; + + childrenXOffset[parentId] += nodeWidth; + + positionedNodes = positionedNodes.set(nodeId, node.merge(makeMap({ + color: nodeColor, + x: nodeX, + y: nodeY, + width: nodeWidth, + height: nodeHeight, + consumption: nodeConsumption, + withCapacity: layerDef.withCapacity, + meta: node, + }))); + }); + + return positionedNodes; + } + ) +); diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js index 86d8bf465e..ef82bd17b9 100644 --- a/client/app/scripts/selectors/resource-view/layout.js +++ b/client/app/scripts/selectors/resource-view/layout.js @@ -1,82 +1,38 @@ -import { createSelector } from 'reselect'; -// import { createMapSelector } from 'reselect-map'; -import { fromJS, Map as makeMap } from 'immutable'; +import { createSelector, createStructuredSelector } from 'reselect'; +import { fromJS } from 'immutable'; -import { resourcesLayers } from '../../constants/styles'; -import { getNodeColor } from '../../utils/color-utils'; +import { layerNodesSelectorFactory } from './layer-factory'; -const basePseudoId = 'base'; - -// TODO: Make this variable -const getCPUMetric = node => (node.get('metrics') || makeMap()).find(m => m.get('label') === 'CPU'); +// TODO: Make this variable. +const layersTopologyIdsSelector = createSelector( + [], + () => fromJS(['hosts', 'containers', 'processes']) +); -// TODO: Parse this logic into multiple smarter selectors -export const layoutNodesSelector = createSelector( +const layoutNodesByTopologyMetaSelector = createSelector( [ - state => state.get('nodesByTopology'), + layersTopologyIdsSelector, ], - (nodesByTopology) => { - const result = []; - const childrenXOffset = { [basePseudoId]: 0 }; - let prevTopologyId = null; - let y = 0; - - resourcesLayers.forEach((layerDef, layerIndex) => { - y -= layerDef.frameHeight + layerDef.verticalPadding; - - const nodes = nodesByTopology.get(layerDef.topologyId); - if (!nodes) return; - - nodes.forEach((node) => { - const metric = getCPUMetric(node); - if (!metric) return; - - const nodeId = node.get('id'); - const nodeColor = getNodeColor(node.get('rank'), node.get('label'), node.get('pseudo')); - - const totalCapacity = metric.get('max'); - const absoluteConsumption = metric.get('value'); - const relativeConsumption = absoluteConsumption / totalCapacity; - const nodeConsumption = layerDef.withCapacity ? relativeConsumption : 1; - - const nodeWidth = layerDef.withCapacity ? totalCapacity : absoluteConsumption; - const nodeHeight = layerDef.frameHeight; - - const shiftX = nodeWidth + layerDef.horizontalPadding; - const parents = node.get('parents') || makeMap(); - const parent = parents.find(p => p.get('topologyId') === prevTopologyId); - const parentId = parent ? parent.get('id') : basePseudoId; + (layersTopologyIds) => { + const layerSelectorsMap = {}; + let prevSelector = () => null; - const nodeY = y; - const nodeX = childrenXOffset[parentId]; - // NOTE: We don't handle uncontained yet. - if (parentId === basePseudoId && layerIndex > 0) return; - - // console.log(nodeX, parentId); - // TODO: Remove. - if (nodeX === undefined) return; - - childrenXOffset[parentId] += shiftX; - childrenXOffset[nodeId] = nodeX; - - result.push(makeMap({ - id: nodeId, - color: nodeColor, - x: nodeX, - y: nodeY, - width: nodeWidth, - height: nodeHeight, - consumption: nodeConsumption, - withCapacity: layerDef.withCapacity, - label: node.get('label'), - meta: node, - })); - }); - - prevTopologyId = layerDef.topologyId; + layersTopologyIds.forEach((topId) => { + layerSelectorsMap[topId] = layerNodesSelectorFactory(topId, prevSelector); + prevSelector = layerSelectorsMap[topId]; }); - return fromJS(result); + return createStructuredSelector(layerSelectorsMap); } ); + +export const layoutNodesSelector = createSelector( + [ + state => layoutNodesByTopologyMetaSelector(state)(state), + layersTopologyIdsSelector, + ], + (layoutNodesByTopology, layersTopologyIds) => ( + layersTopologyIds.flatMap(topId => layoutNodesByTopology[topId].toList()) + ) +); From de3d99fabe09ee9a0ebc940bdc2bb008becac2c1 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 10 Mar 2017 22:56:27 +0100 Subject: [PATCH 11/32] Added sticky labels to the resource boxes --- .../scripts/charts/node-resource-metric.js | 43 +++++-- client/app/scripts/charts/nodes-chart.js | 14 +-- client/app/scripts/charts/nodes-resources.js | 59 ++++++++++ client/app/scripts/charts/resource-view.js | 39 ------ .../components/cachable-zoom-wrapper.js | 17 ++- client/app/scripts/components/logo.js | 111 +++++++++--------- client/app/scripts/components/nodes.js | 19 ++- .../selectors/resource-view/default-zoom.js | 2 +- .../selectors/resource-view/layer-factory.js | 3 +- client/app/styles/_base.scss | 11 ++ 10 files changed, 195 insertions(+), 123 deletions(-) create mode 100644 client/app/scripts/charts/nodes-resources.js delete mode 100644 client/app/scripts/charts/resource-view.js diff --git a/client/app/scripts/charts/node-resource-metric.js b/client/app/scripts/charts/node-resource-metric.js index 09ed93c8d7..d1df3cfcad 100644 --- a/client/app/scripts/charts/node-resource-metric.js +++ b/client/app/scripts/charts/node-resource-metric.js @@ -14,33 +14,62 @@ export default class NodeResourceMetric extends React.Component { console.log(this.props.meta.toJS()); } + // renderLabel() { + // return ( + // + //
+ // {this.props.label} + //
+ //
+ // ); + // } + render() { - const { label, color, width, height, x, y, consumption } = this.props; + const { label, info, color, width, height, x, y, consumption } = this.props; const innerHeight = height * consumption; - const transform = `translate(${x},${y})`; + const labelX = Math.max(0, x) + 10; + const labelWidth = Math.max(0, (x + width) - (labelX + 10)); + const labelShown = (labelWidth > 20); return ( - - {label} + + {info} + {labelShown && +
+ {label} +
+
}
); } diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 6f19af220e..dfe3948c80 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -2,11 +2,9 @@ import React from 'react'; import { connect } from 'react-redux'; import Logo from '../components/logo'; -import ResourceView from './resource-view'; import NodesChartElements from './nodes-chart-elements'; import CachableZoomWrapper from '../components/cachable-zoom-wrapper'; import { clickBackground } from '../actions/app-actions'; -import { isGraphViewModeSelector } from '../selectors/topology'; class NodesChart extends React.Component { @@ -17,6 +15,7 @@ class NodesChart extends React.Component { } render() { + // TODO: What to do with empty? const { isGraphViewMode, isEmpty, selectedNodeId } = this.props; const markerOffset = selectedNodeId ? '35' : '40'; const markerSize = selectedNodeId ? '10' : '30'; @@ -26,8 +25,7 @@ class NodesChart extends React.Component {
+ className={svgClassNames} onClick={this.handleMouseClick}> + orient="auto"> - - - + {isGraphViewMode ? : } @@ -63,7 +58,6 @@ class NodesChart extends React.Component { function mapStateToProps(state) { return { - isGraphViewMode: isGraphViewModeSelector(state), selectedNodeId: state.get('selectedNodeId'), }; } diff --git a/client/app/scripts/charts/nodes-resources.js b/client/app/scripts/charts/nodes-resources.js new file mode 100644 index 0000000000..e3e8fba392 --- /dev/null +++ b/client/app/scripts/charts/nodes-resources.js @@ -0,0 +1,59 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import Logo from '../components/logo'; +import { layoutNodesSelector } from '../selectors/resource-view/layout'; +import CachableZoomWrapper from '../components/cachable-zoom-wrapper'; +import NodeResourceMetric from './node-resource-metric'; + + +const applyTransform = (node, { scaleX, scaleY, translateX, translateY }) => ( + node.merge({ + x: (node.get('x') * scaleX) + translateX, + y: (node.get('y') * scaleY) + translateY, + width: node.get('width') * scaleX, + height: node.get('height') * scaleY, + }) +); + +class NodesResources extends React.Component { + render() { + return ( +
+ + + + {transform => ( + this.props.layoutNodes.map(node => applyTransform(node, transform)).map(node => ( + + )))} + + +
+ ); + } +} + +function mapStateToProps(state) { + return { + layoutNodes: layoutNodesSelector(state), + }; +} + +export default connect( + mapStateToProps +)(NodesResources); diff --git a/client/app/scripts/charts/resource-view.js b/client/app/scripts/charts/resource-view.js deleted file mode 100644 index e1d735299a..0000000000 --- a/client/app/scripts/charts/resource-view.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; - -import { layoutNodesSelector } from '../selectors/resource-view/layout'; -import NodeResourceMetric from './node-resource-metric'; - - -class ResourceView extends React.Component { - render() { - return ( - - {this.props.layoutNodes.map(node => ( - - ))} - - ); - } -} - -function mapStateToProps(state) { - return { - layoutNodes: layoutNodesSelector(state), - }; -} - -export default connect( - mapStateToProps -)(ResourceView); diff --git a/client/app/scripts/components/cachable-zoom-wrapper.js b/client/app/scripts/components/cachable-zoom-wrapper.js index 72093fba8d..08fe357ebe 100644 --- a/client/app/scripts/components/cachable-zoom-wrapper.js +++ b/client/app/scripts/components/cachable-zoom-wrapper.js @@ -68,13 +68,14 @@ class CachableZoomWrapper extends React.Component { } render() { - // Not passing transform into child components for perf reasons. + const { children, forwardTransform } = this.props; const { translateX, translateY, scaleX, scaleY } = this.state; const transform = `translate(${translateX},${translateY}) scale(${scaleX},${scaleY})`; + // Not passing transform into child components by default for perf reasons. return ( - - {this.props.children} + + {forwardTransform ? children(this.state) : children} ); } @@ -120,15 +121,19 @@ class CachableZoomWrapper extends React.Component { zoomed() { if (!this.props.disabled) { - const updatedState = { + const updatedState = this.cachableState({ scaleX: d3Event.transform.k, scaleY: d3Event.transform.k, translateX: d3Event.transform.x, translateY: d3Event.transform.y, - }; + }); - this.setState(this.cachableState(updatedState)); + this.setState(updatedState); this.debouncedCacheZoom(); + + if (this.props.zoomUpdated) { + this.props.zoomUpdated(this.state); + } } } } diff --git a/client/app/scripts/components/logo.js b/client/app/scripts/components/logo.js index 8e845dbdd9..9d9a54a8f1 100644 --- a/client/app/scripts/components/logo.js +++ b/client/app/scripts/components/logo.js @@ -2,58 +2,61 @@ /* eslint max-len: "off" */ import React from 'react'; -export default function Logo() { - return ( - - - - - - - - - - - - - - - - - - - ); +export default class Logo extends React.Component { + render() { + const { transform = '' } = this.props; + return ( + + + + + + + + + + + + + + + + + + + ); + } } diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js index fd045ffd79..9c8e44dbaf 100644 --- a/client/app/scripts/components/nodes.js +++ b/client/app/scripts/components/nodes.js @@ -4,12 +4,17 @@ import { debounce } from 'lodash'; import NodesChart from '../charts/nodes-chart'; import NodesGrid from '../charts/nodes-grid'; +import NodesResources from '../charts/nodes-resources'; import NodesError from '../charts/nodes-error'; import DelayedShow from '../utils/delayed-show'; import { Loading, getNodeType } from './loading'; import { isTopologyEmpty } from '../utils/topology-utils'; import { setViewportDimensions } from '../actions/app-actions'; -import { isTableViewModeSelector } from '../selectors/topology'; +import { + isGraphViewModeSelector, + isTableViewModeSelector, + isResourceViewModeSelector, +} from '../selectors/topology'; import { VIEWPORT_RESIZE_DEBOUNCE_INTERVAL } from '../constants/timer'; @@ -48,10 +53,10 @@ class Nodes extends React.Component { } render() { - const { topologyEmpty, isTableViewMode, topologiesLoaded, nodesLoaded, topologies, - currentTopology } = this.props; + const { topologyEmpty, topologiesLoaded, nodesLoaded, topologies, currentTopology, + isGraphViewMode, isTableViewMode, isResourceViewMode } = this.props; - // TODO: Get rid of 'grid'. + // TODO: Rename view mode components. return (
@@ -62,7 +67,9 @@ class Nodes extends React.Component { {EmptyTopologyError(topologiesLoaded && nodesLoaded && topologyEmpty)} - {isTableViewMode ? : } + {isGraphViewMode && } + {isTableViewMode && } + {isResourceViewMode && }
); } @@ -77,7 +84,9 @@ class Nodes extends React.Component { function mapStateToProps(state) { return { + isGraphViewMode: isGraphViewModeSelector(state), isTableViewMode: isTableViewModeSelector(state), + isResourceViewMode: isResourceViewModeSelector(state), currentTopology: state.get('currentTopology'), nodesLoaded: state.get('nodesLoaded'), topologies: state.get('topologies'), diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index 65c9b92693..a51473814d 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -26,7 +26,7 @@ export const resourcesDefaultZoomSelector = createSelector( const scaleX = (width / (xMax - xMin)) * 0.9; const scaleY = (height / (yMax - yMin)) * 0.9; const minScale = scaleX * 0.5; - const maxScale = scaleX * 1000; + const maxScale = scaleX * 2000; // This translation puts the graph in the center of the viewport, respecting the margins. const translateX = ((width - ((xMax + xMin) * scaleX)) / 2) + CANVAS_MARGINS.left; diff --git a/client/app/scripts/selectors/resource-view/layer-factory.js b/client/app/scripts/selectors/resource-view/layer-factory.js index 24cd2af810..638eed971d 100644 --- a/client/app/scripts/selectors/resource-view/layer-factory.js +++ b/client/app/scripts/selectors/resource-view/layer-factory.js @@ -33,7 +33,7 @@ export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) const nodeColor = getNodeColor(node.get('rank'), node.get('label'), node.get('pseudo')); const totalCapacity = metric.get('max'); - const absoluteConsumption = metric.get('value'); + const absoluteConsumption = metric.get('value') / (topologyId === 'processes' ? 4 : 1); const relativeConsumption = absoluteConsumption / totalCapacity; const nodeConsumption = layerDef.withCapacity ? relativeConsumption : 1; @@ -66,6 +66,7 @@ export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) height: nodeHeight, consumption: nodeConsumption, withCapacity: layerDef.withCapacity, + info: `CPU usage: ${absoluteConsumption}%`, meta: node, }))); }); diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index cbb0bb1e82..cfabcee8c8 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -937,6 +937,17 @@ } } +.node-resource-metric { + .node-label-wrapper { + background-color: rgba(white, 0.6); + border-radius: 2px; + cursor: default; + display: block; + font-size: 15px; + padding: 5px; + } +} + // This part sets the styles only for the 'real' node details table, not applying // them to the nodes grid, because there we control hovering from the JS. // NOTE: Maybe it would be nice to separate the class names between the two places From ce1282c076d77b084f1b0eba09fbcfdd08db59bc Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 10 Mar 2017 23:34:08 +0100 Subject: [PATCH 12/32] Added panning translation limits in the resource view --- .../components/cachable-zoom-wrapper.js | 27 ++++++++++++------- .../selectors/graph-view/default-zoom.js | 18 ++++++++++++- .../selectors/resource-view/default-zoom.js | 15 +++++++++-- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/client/app/scripts/components/cachable-zoom-wrapper.js b/client/app/scripts/components/cachable-zoom-wrapper.js index 08fe357ebe..4ce25d0303 100644 --- a/client/app/scripts/components/cachable-zoom-wrapper.js +++ b/client/app/scripts/components/cachable-zoom-wrapper.js @@ -18,12 +18,16 @@ class CachableZoomWrapper extends React.Component { super(props, context); this.state = { + minTranslateX: 0, + maxTranslateX: 0, + minTranslateY: 0, + maxTranslateY: 0, + translateX: 0, + translateY: 0, minScale: 1, maxScale: 1, scaleX: 1, scaleY: 1, - translateX: 0, - translateY: 0, }; this.debouncedCacheZoom = debounce(this.cacheZoom.bind(this), ZOOM_CACHE_DEBOUNCE_INTERVAL); @@ -74,7 +78,7 @@ class CachableZoomWrapper extends React.Component { // Not passing transform into child components by default for perf reasons. return ( - + {forwardTransform ? children(this.state) : children} ); @@ -89,7 +93,11 @@ class CachableZoomWrapper extends React.Component { } cachableState(state = this.state) { - let cachableFields = ['minScale', 'maxScale']; + let cachableFields = [ + 'minTranslateX', 'maxTranslateX', + 'minTranslateY', 'maxTranslateY', + 'minScale', 'maxScale' + ]; if (!this.props.fixHorizontal) { cachableFields = cachableFields.concat(['scaleX', 'translateX']); } @@ -108,7 +116,12 @@ class CachableZoomWrapper extends React.Component { const zoomState = props.layoutZoom.toJS(); // Restore the zooming settings - this.zoom = this.zoom.scaleExtent([zoomState.minScale, zoomState.maxScale]); + this.zoom = this.zoom + .scaleExtent([zoomState.minScale, zoomState.maxScale]) + .translateExtent([ + [zoomState.minTranslateX, zoomState.minTranslateY], + [zoomState.maxTranslateX, zoomState.maxTranslateY], + ]); this.svg.call(this.zoom.transform, zoomIdentity .translate(zoomState.translateX, zoomState.translateY) .scale(zoomState.scaleX, zoomState.scaleY)); @@ -130,10 +143,6 @@ class CachableZoomWrapper extends React.Component { this.setState(updatedState); this.debouncedCacheZoom(); - - if (this.props.zoomUpdated) { - this.props.zoomUpdated(this.state); - } } } } diff --git a/client/app/scripts/selectors/graph-view/default-zoom.js b/client/app/scripts/selectors/graph-view/default-zoom.js index 8406750103..8fb77ce0de 100644 --- a/client/app/scripts/selectors/graph-view/default-zoom.js +++ b/client/app/scripts/selectors/graph-view/default-zoom.js @@ -6,6 +6,8 @@ import { viewportWidthSelector, viewportHeightSelector } from '../viewport'; import { graphNodesSelector } from './graph'; +const MARGIN_FACTOR = 10; + // Compute the default zoom settings for the given graph. export const graphDefaultZoomSelector = createSelector( [ @@ -40,6 +42,20 @@ export const graphDefaultZoomSelector = createSelector( const translateX = ((width - ((xMax + xMin) * scale)) / 2) + CANVAS_MARGINS.left; const translateY = ((height - ((yMax + yMin) * scale)) / 2) + CANVAS_MARGINS.top; - return makeMap({ scaleX: scale, scaleY: scale, minScale, maxScale, translateX, translateY }); + const xMargin = (xMax - xMin) * MARGIN_FACTOR; + const yMargin = (yMax - yMin) * MARGIN_FACTOR; + + return makeMap({ + minTranslateX: xMin - xMargin, + maxTranslateX: xMax + xMargin, + minTranslateY: yMin - yMargin, + maxTranslateY: yMax + yMargin, + translateX, + translateY, + minScale, + maxScale, + scaleX: scale, + scaleY: scale, + }); } ); diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index a51473814d..f92cc6be44 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -25,13 +25,24 @@ export const resourcesDefaultZoomSelector = createSelector( const scaleX = (width / (xMax - xMin)) * 0.9; const scaleY = (height / (yMax - yMin)) * 0.9; - const minScale = scaleX * 0.5; + const minScale = scaleX * 1; const maxScale = scaleX * 2000; // This translation puts the graph in the center of the viewport, respecting the margins. const translateX = ((width - ((xMax + xMin) * scaleX)) / 2) + CANVAS_MARGINS.left; const translateY = ((height - ((yMax + yMin) * scaleY)) / 2) + CANVAS_MARGINS.top; - return makeMap({ scaleX, scaleY, minScale, maxScale, translateX, translateY }); + return makeMap({ + minTranslateX: xMin, + maxTranslateX: xMax, + minTranslateY: yMin, + maxTranslateY: yMax, + translateX, + translateY, + minScale, + maxScale, + scaleX, + scaleY, + }); } ); From e05302e9432501318cb4835a093f92c40a2e203e Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 10 Mar 2017 23:59:10 +0100 Subject: [PATCH 13/32] Renamed GridModeSelector -> ViewModeSelector --- client/app/scripts/components/app.js | 4 ++-- .../scripts/components/cachable-zoom-wrapper.js | 1 + ...id-mode-selector.js => view-mode-selector.js} | 16 +++++++--------- .../selectors/resource-view/default-zoom.js | 6 +++--- client/app/styles/_base.scss | 9 ++++----- 5 files changed, 17 insertions(+), 19 deletions(-) rename client/app/scripts/components/{grid-mode-selector.js => view-mode-selector.js} (72%) diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index c6ba66a051..724fc02605 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -16,7 +16,7 @@ import { focusSearch, pinNextMetric, hitBackspace, hitEnter, hitEsc, unpinMetric selectMetric, toggleHelp, toggleGridMode, shutdown } from '../actions/app-actions'; import Details from './details'; import Nodes from './nodes'; -import GridModeSelector from './grid-mode-selector'; +import ViewModeSelector from './view-mode-selector'; import MetricSelector from './metric-selector'; import NetworkSelector from './networks-selector'; import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar'; @@ -130,7 +130,7 @@ class App extends React.Component {
- + diff --git a/client/app/scripts/components/cachable-zoom-wrapper.js b/client/app/scripts/components/cachable-zoom-wrapper.js index 4ce25d0303..7d9698eb5a 100644 --- a/client/app/scripts/components/cachable-zoom-wrapper.js +++ b/client/app/scripts/components/cachable-zoom-wrapper.js @@ -118,6 +118,7 @@ class CachableZoomWrapper extends React.Component { // Restore the zooming settings this.zoom = this.zoom .scaleExtent([zoomState.minScale, zoomState.maxScale]) + .extent([[50, 50], [200, 600]]) .translateExtent([ [zoomState.minTranslateX, zoomState.minTranslateY], [zoomState.maxTranslateX, zoomState.maxTranslateY], diff --git a/client/app/scripts/components/grid-mode-selector.js b/client/app/scripts/components/view-mode-selector.js similarity index 72% rename from client/app/scripts/components/grid-mode-selector.js rename to client/app/scripts/components/view-mode-selector.js index b97fe349de..698d737e64 100644 --- a/client/app/scripts/components/grid-mode-selector.js +++ b/client/app/scripts/components/view-mode-selector.js @@ -10,8 +10,8 @@ import { } from '../selectors/topology'; const Item = (icons, label, isSelected, onClick) => { - const className = classNames('grid-mode-selector-action', { - 'grid-mode-selector-action-selected': isSelected + const className = classNames('view-mode-selector-action', { + 'view-mode-selector-action-selected': isSelected }); return (
{ ); }; -class GridModeSelector extends React.Component { +class ViewModeSelector extends React.Component { render() { const { isGraphViewMode, isTableViewMode, isResourceViewMode } = this.props; return ( -
-
+
+
{Item('fa fa-share-alt', 'Graph', isGraphViewMode, this.props.setGraphView)} {Item('fa fa-table', 'Table', isTableViewMode, this.props.setTableView)} -
-
- {Item('fa fa-bar-chart', 'Resource view', isResourceViewMode, this.props.setResourceView)} + {Item('fa fa-bar-chart', 'Resources', isResourceViewMode, this.props.setResourceView)}
); @@ -52,4 +50,4 @@ function mapStateToProps(state) { export default connect( mapStateToProps, { setGraphView, setTableView, setResourceView } -)(GridModeSelector); +)(ViewModeSelector); diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index f92cc6be44..ee633e5eca 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -23,10 +23,10 @@ export const resourcesDefaultZoomSelector = createSelector( const xMax = layoutNodes.map(n => n.get('x') + n.get('width')).max(); const yMax = layoutNodes.map(n => n.get('y') + n.get('height')).max(); - const scaleX = (width / (xMax - xMin)) * 0.9; - const scaleY = (height / (yMax - yMin)) * 0.9; - const minScale = scaleX * 1; + const scaleX = (width / (xMax - xMin)); + const scaleY = (height / (yMax - yMin)); const maxScale = scaleX * 2000; + const minScale = scaleX; // This translation puts the graph in the center of the viewport, respecting the margins. const translateX = ((width - ((xMax + xMin) * scaleX)) / 2) + CANVAS_MARGINS.left; diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index cfabcee8c8..3c29a620cd 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -1192,7 +1192,7 @@ } } -.topology-option, .metric-selector, .network-selector, .grid-mode-selector { +.topology-option, .metric-selector, .network-selector, .view-mode-selector { color: $text-secondary-color; margin: 6px 0; @@ -1238,8 +1238,7 @@ } } -.grid-mode-selector { - +.view-mode-selector { margin-top: 8px; margin-left: 8px; min-width: 161px; @@ -1252,7 +1251,7 @@ &:first-child, &:last-child { - .grid-mode-selector-action { + .view-mode-selector-action { border-radius: 0; } } @@ -1279,7 +1278,7 @@ } } -.grid-mode-selector .fa { +.view-mode-selector .fa { margin-right: 4px; margin-left: 0; color: $text-secondary-color; From c22599307aa775f1db2c02c3036c0d15d98a0e04 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Sat, 11 Mar 2017 00:14:18 +0100 Subject: [PATCH 14/32] Polished the topology resource view selection logic --- .../app/scripts/components/view-mode-selector.js | 14 ++++++++++++-- client/app/scripts/constants/resources.js | 5 +++++ .../app/scripts/selectors/resource-view/layout.js | 10 ++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 client/app/scripts/constants/resources.js diff --git a/client/app/scripts/components/view-mode-selector.js b/client/app/scripts/components/view-mode-selector.js index 698d737e64..66b32889f6 100644 --- a/client/app/scripts/components/view-mode-selector.js +++ b/client/app/scripts/components/view-mode-selector.js @@ -3,12 +3,14 @@ import { connect } from 'react-redux'; import classNames from 'classnames'; import { setGraphView, setTableView, setResourceView } from '../actions/app-actions'; +import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; import { isGraphViewModeSelector, isTableViewModeSelector, isResourceViewModeSelector, } from '../selectors/topology'; + const Item = (icons, label, isSelected, onClick) => { const className = classNames('view-mode-selector-action', { 'view-mode-selector-action-selected': isSelected @@ -24,15 +26,22 @@ const Item = (icons, label, isSelected, onClick) => { }; class ViewModeSelector extends React.Component { + componentWillReceiveProps(nextProps) { + if (nextProps.isResourceViewMode && !nextProps.hasResourceView) { + nextProps.setGraphView(); + } + } + render() { - const { isGraphViewMode, isTableViewMode, isResourceViewMode } = this.props; + const { isGraphViewMode, isTableViewMode, isResourceViewMode, hasResourceView } = this.props; return (
{Item('fa fa-share-alt', 'Graph', isGraphViewMode, this.props.setGraphView)} {Item('fa fa-table', 'Table', isTableViewMode, this.props.setTableView)} - {Item('fa fa-bar-chart', 'Resources', isResourceViewMode, this.props.setResourceView)} + {hasResourceView && + Item('fa fa-bar-chart', 'Resources', isResourceViewMode, this.props.setResourceView)}
); @@ -44,6 +53,7 @@ function mapStateToProps(state) { isGraphViewMode: isGraphViewModeSelector(state), isTableViewMode: isTableViewModeSelector(state), isResourceViewMode: isResourceViewModeSelector(state), + hasResourceView: !layersTopologyIdsSelector(state).isEmpty(), }; } diff --git a/client/app/scripts/constants/resources.js b/client/app/scripts/constants/resources.js new file mode 100644 index 0000000000..6eb8a657d4 --- /dev/null +++ b/client/app/scripts/constants/resources.js @@ -0,0 +1,5 @@ + +// TODO: Consider fetching this from the backend. +export const resourceViewLayers = { + hosts: ['hosts', 'containers', 'processes'] +}; diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js index ef82bd17b9..c8389e7076 100644 --- a/client/app/scripts/selectors/resource-view/layout.js +++ b/client/app/scripts/selectors/resource-view/layout.js @@ -2,12 +2,14 @@ import { createSelector, createStructuredSelector } from 'reselect'; import { fromJS } from 'immutable'; import { layerNodesSelectorFactory } from './layer-factory'; +import { resourceViewLayers } from '../../constants/resources'; -// TODO: Make this variable. -const layersTopologyIdsSelector = createSelector( - [], - () => fromJS(['hosts', 'containers', 'processes']) +export const layersTopologyIdsSelector = createSelector( + [ + state => state.get('currentTopologyId'), + ], + topologyId => fromJS(resourceViewLayers[topologyId] || []) ); const layoutNodesByTopologyMetaSelector = createSelector( From 04bfca2949ec35a83604ee896476c3dcd08d51da Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Sat, 11 Mar 2017 00:19:06 +0100 Subject: [PATCH 15/32] Search bar hidden in the resource view --- .../app/scripts/charts/node-resource-box.js | 50 +++++++++++ .../app/scripts/charts/node-resource-label.js | 23 +++++ .../scripts/charts/node-resource-metric.js | 76 ----------------- .../scripts/charts/nodes-resources-layer.js | 85 +++++++++++++++++++ client/app/scripts/charts/nodes-resources.js | 42 +++------ client/app/scripts/components/search.js | 6 +- client/app/scripts/constants/styles.js | 2 + .../selectors/resource-view/layer-factory.js | 7 +- .../scripts/selectors/resource-view/layout.js | 2 +- client/app/styles/_base.scss | 4 +- 10 files changed, 184 insertions(+), 113 deletions(-) create mode 100644 client/app/scripts/charts/node-resource-box.js create mode 100644 client/app/scripts/charts/node-resource-label.js delete mode 100644 client/app/scripts/charts/node-resource-metric.js create mode 100644 client/app/scripts/charts/nodes-resources-layer.js diff --git a/client/app/scripts/charts/node-resource-box.js b/client/app/scripts/charts/node-resource-box.js new file mode 100644 index 0000000000..2b80522902 --- /dev/null +++ b/client/app/scripts/charts/node-resource-box.js @@ -0,0 +1,50 @@ +import React from 'react'; + +const frameFill = 'rgba(100, 100, 100, 0.2)'; +const frameStroke = 'rgba(255, 255, 255, 1)'; +const frameStrokeWidth = 1.5; + +export default class NodeResourceBox extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleMouseClick = this.handleMouseClick.bind(this); + } + + handleMouseClick() { + console.log(this.props.meta.toJS()); + } + + render() { + const { info, color, width, height, x, y, consumption } = this.props; + const innerHeight = height * consumption; + + return ( + + {info} + + + + ); + } +} diff --git a/client/app/scripts/charts/node-resource-label.js b/client/app/scripts/charts/node-resource-label.js new file mode 100644 index 0000000000..7d10e61d2c --- /dev/null +++ b/client/app/scripts/charts/node-resource-label.js @@ -0,0 +1,23 @@ +import React from 'react'; + + +export default class NodeResourceBox extends React.Component { + render() { + const { label, width, x, y } = this.props; + + return ( + + +
+ {label} +
+
+
+ ); + } +} diff --git a/client/app/scripts/charts/node-resource-metric.js b/client/app/scripts/charts/node-resource-metric.js deleted file mode 100644 index d1df3cfcad..0000000000 --- a/client/app/scripts/charts/node-resource-metric.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; - -const frameFill = 'rgba(100, 100, 100, 0.2)'; -const frameStroke = 'rgba(255, 255, 255, 1)'; -const frameStrokeWidth = 1.5; - -export default class NodeResourceMetric extends React.Component { - constructor(props, context) { - super(props, context); - this.handleMouseClick = this.handleMouseClick.bind(this); - } - - handleMouseClick() { - console.log(this.props.meta.toJS()); - } - - // renderLabel() { - // return ( - // - //
- // {this.props.label} - //
- //
- // ); - // } - - render() { - const { label, info, color, width, height, x, y, consumption } = this.props; - const innerHeight = height * consumption; - const labelX = Math.max(0, x) + 10; - const labelWidth = Math.max(0, (x + width) - (labelX + 10)); - const labelShown = (labelWidth > 20); - - return ( - - {info} - - - {labelShown && -
- {label} -
-
} -
- ); - } -} diff --git a/client/app/scripts/charts/nodes-resources-layer.js b/client/app/scripts/charts/nodes-resources-layer.js new file mode 100644 index 0000000000..81062e35dc --- /dev/null +++ b/client/app/scripts/charts/nodes-resources-layer.js @@ -0,0 +1,85 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Map as makeMap } from 'immutable'; + +import { layoutNodesByTopologyMetaSelector } from '../selectors/resource-view/layout'; +import NodeResourceBox from './node-resource-box'; +import NodeResourceLabel from './node-resource-label'; + + +const PADDING = 10; + +const stringifiedTransform = ({ scaleX, scaleY, translateX, translateY }) => ( + `translate(${translateX},${translateY}) scale(${scaleX},${scaleY})` +); + +const getPositionedLabels = (nodes, transform) => { + const { scaleX, scaleY, translateX, translateY } = transform; + return nodes.map((node) => { + const nodeX = (node.get('x') * scaleX) + translateX; + const nodeY = (node.get('y') * scaleY) + translateY; + const nodeWidth = node.get('width') * scaleX; + + const labelY = nodeY + PADDING; + const labelX = Math.max(0, nodeX) + PADDING; + const labelWidth = (nodeX + nodeWidth) - PADDING - labelX; + + if (labelWidth < 20) return makeMap(); + + return makeMap({ + id: node.get('id'), + label: node.get('label'), + width: labelWidth, + x: labelX, + y: labelY, + }); + }).filter(label => !label.isEmpty()); +}; + +class NodesResourcesLayer extends React.Component { + render() { + const { transform, nodes, labels } = this.props; + return ( + + + {nodes.map(node => ( + + ))} + + + {labels.map(label => ( + + ))} + + + ); + } +} + +function mapStateToProps(state, props) { + const nodes = layoutNodesByTopologyMetaSelector(state)(state)[props.topologyId]; + // TODO: Move to selectors? + const labels = getPositionedLabels(nodes, props.transform); + return { nodes, labels }; +} + +export default connect( + mapStateToProps +)(NodesResourcesLayer); diff --git a/client/app/scripts/charts/nodes-resources.js b/client/app/scripts/charts/nodes-resources.js index e3e8fba392..e812156b73 100644 --- a/client/app/scripts/charts/nodes-resources.js +++ b/client/app/scripts/charts/nodes-resources.js @@ -2,21 +2,22 @@ import React from 'react'; import { connect } from 'react-redux'; import Logo from '../components/logo'; -import { layoutNodesSelector } from '../selectors/resource-view/layout'; +import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; import CachableZoomWrapper from '../components/cachable-zoom-wrapper'; -import NodeResourceMetric from './node-resource-metric'; +import NodesResourcesLayer from './nodes-resources-layer'; -const applyTransform = (node, { scaleX, scaleY, translateX, translateY }) => ( - node.merge({ - x: (node.get('x') * scaleX) + translateX, - y: (node.get('y') * scaleY) + translateY, - width: node.get('width') * scaleX, - height: node.get('height') * scaleY, - }) -); - class NodesResources extends React.Component { + renderLayers(transform) { + return this.props.layersTopologyIds.map(topologyId => ( + + )); + } + render() { return (
@@ -25,22 +26,7 @@ class NodesResources extends React.Component { id="nodes-chart-canvas"> - {transform => ( - this.props.layoutNodes.map(node => applyTransform(node, transform)).map(node => ( - - )))} + {transform => this.renderLayers(transform)}
@@ -50,7 +36,7 @@ class NodesResources extends React.Component { function mapStateToProps(state) { return { - layoutNodes: layoutNodesSelector(state), + layersTopologyIds: layersTopologyIdsSelector(state), }; } diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index 5a9d9c3196..7572b67608 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -5,6 +5,7 @@ import { debounce } from 'lodash'; import { blurSearch, doSearch, focusSearch, showHelp } from '../actions/app-actions'; import { searchMatchCountByTopologySelector } from '../selectors/search'; +import { isResourceViewModeSelector } from '../selectors/topology'; import { slugify } from '../utils/string-utils'; import { isTopologyEmpty } from '../utils/topology-utils'; import SearchItem from './search-item'; @@ -102,7 +103,7 @@ class Search extends React.Component { } render() { - const { nodes, pinnedSearches, searchFocused, searchMatchCountByTopology, + const { nodes, pinnedSearches, searchFocused, searchMatchCountByTopology, isResourceViewMode, searchQuery, topologiesLoaded, onClickHelp, inputId = 'search' } = this.props; const disabled = this.props.isTopologyEmpty; const matchCount = searchMatchCountByTopology @@ -111,7 +112,7 @@ class Search extends React.Component { // manual clear (null) has priority, then props, then state const value = this.state.value === null ? '' : this.state.value || searchQuery || ''; const classNames = classnames('search', 'hideable', { - hide: !topologiesLoaded, + hide: !topologiesLoaded || isResourceViewMode, 'search-pinned': showPinnedSearches, 'search-matched': matchCount, 'search-filled': value, @@ -153,6 +154,7 @@ class Search extends React.Component { export default connect( state => ({ nodes: state.get('nodes'), + isResourceViewMode: isResourceViewModeSelector(state), isTopologyEmpty: isTopologyEmpty(state), topologiesLoaded: state.get('topologiesLoaded'), pinnedSearches: state.get('pinnedSearches'), diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index 56c8dfdc13..260a8d0758 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -16,6 +16,8 @@ export const CANVAS_MARGINS = { bottom: 0, }; +export const RESOURCES_LAYER_HEIGHT = 150; + // Node shapes export const NODE_SHAPE_HIGHLIGHT_RADIUS = 70; export const NODE_SHAPE_BORDER_RADIUS = 50; diff --git a/client/app/scripts/selectors/resource-view/layer-factory.js b/client/app/scripts/selectors/resource-view/layer-factory.js index 638eed971d..cd3f32486d 100644 --- a/client/app/scripts/selectors/resource-view/layer-factory.js +++ b/client/app/scripts/selectors/resource-view/layer-factory.js @@ -1,7 +1,7 @@ import { createSelector, createStructuredSelector } from 'reselect'; import { fromJS, Map as makeMap } from 'immutable'; -import { layersDefs } from '../../constants/styles'; +import { layersDefs, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; import { getNodeColor } from '../../utils/color-utils'; /* eslint no-unused-vars: 0 */ /* eslint no-nested-ternary: 0 */ @@ -38,7 +38,6 @@ export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) const nodeConsumption = layerDef.withCapacity ? relativeConsumption : 1; const nodeWidth = layerDef.withCapacity ? totalCapacity : absoluteConsumption; - const nodeHeight = layerDef.frameHeight; const parents = node.get('parents') || makeMap(); const parent = parents.find(p => p.get('topologyId') === layerDef.parentTopologyId); @@ -50,7 +49,7 @@ export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) childrenXOffset[parentId] = childrenXOffset[parentId] || parentLayerNodes.getIn([parentId, 'x'], 0); const nodeX = childrenXOffset[parentId]; - const nodeY = topologyId === 'hosts' ? 0 : (topologyId === 'containers' ? -160 : -265); + const nodeY = parentLayerNodes.getIn([parentId, 'y'], 0) - RESOURCES_LAYER_HEIGHT; // console.log(nodeX, parentId); // TODO: Remove. @@ -63,7 +62,7 @@ export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) x: nodeX, y: nodeY, width: nodeWidth, - height: nodeHeight, + height: RESOURCES_LAYER_HEIGHT, consumption: nodeConsumption, withCapacity: layerDef.withCapacity, info: `CPU usage: ${absoluteConsumption}%`, diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js index c8389e7076..f3621362db 100644 --- a/client/app/scripts/selectors/resource-view/layout.js +++ b/client/app/scripts/selectors/resource-view/layout.js @@ -12,7 +12,7 @@ export const layersTopologyIdsSelector = createSelector( topologyId => fromJS(resourceViewLayers[topologyId] || []) ); -const layoutNodesByTopologyMetaSelector = createSelector( +export const layoutNodesByTopologyMetaSelector = createSelector( [ layersTopologyIdsSelector, ], diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 3c29a620cd..192840d787 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -937,8 +937,8 @@ } } -.node-resource-metric { - .node-label-wrapper { +.node-resource { + &-label .label-wrapper { background-color: rgba(white, 0.6); border-radius: 2px; cursor: default; From 7cdfcf722a4c5e4e0ff387b047cb8e52fd66cc71 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Sat, 11 Mar 2017 23:21:06 +0100 Subject: [PATCH 16/32] Added per-layer topology names to the resource view --- .../app/scripts/charts/node-resource-box.js | 2 +- .../scripts/charts/nodes-resources-layer.js | 20 ++++++++++--- client/app/scripts/charts/nodes-resources.js | 2 +- .../scripts/components/view-mode-selector.js | 2 +- client/app/scripts/constants/styles.js | 2 ++ .../selectors/resource-view/default-zoom.js | 10 ++++--- .../selectors/resource-view/layer-factory.js | 6 ++-- .../scripts/selectors/resource-view/layers.js | 29 +++++++++++++++++++ .../scripts/selectors/resource-view/layout.js | 10 +------ client/app/styles/_base.scss | 11 +++++++ 10 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 client/app/scripts/selectors/resource-view/layers.js diff --git a/client/app/scripts/charts/node-resource-box.js b/client/app/scripts/charts/node-resource-box.js index 2b80522902..4cc10f36c9 100644 --- a/client/app/scripts/charts/node-resource-box.js +++ b/client/app/scripts/charts/node-resource-box.js @@ -1,6 +1,6 @@ import React from 'react'; -const frameFill = 'rgba(100, 100, 100, 0.2)'; +const frameFill = 'rgba(100, 100, 100, 0.3)'; const frameStroke = 'rgba(255, 255, 255, 1)'; const frameStrokeWidth = 1.5; diff --git a/client/app/scripts/charts/nodes-resources-layer.js b/client/app/scripts/charts/nodes-resources-layer.js index 81062e35dc..fbafd1bdf8 100644 --- a/client/app/scripts/charts/nodes-resources-layer.js +++ b/client/app/scripts/charts/nodes-resources-layer.js @@ -2,7 +2,9 @@ import React from 'react'; import { connect } from 'react-redux'; import { Map as makeMap } from 'immutable'; +import { RESOURCES_LAYER_TITLE_WIDTH, RESOURCES_LAYER_HEIGHT } from '../constants/styles'; import { layoutNodesByTopologyMetaSelector } from '../selectors/resource-view/layout'; +import { layersVerticalPositionSelector } from '../selectors/resource-view/layers'; import NodeResourceBox from './node-resource-box'; import NodeResourceLabel from './node-resource-label'; @@ -21,7 +23,7 @@ const getPositionedLabels = (nodes, transform) => { const nodeWidth = node.get('width') * scaleX; const labelY = nodeY + PADDING; - const labelX = Math.max(0, nodeX) + PADDING; + const labelX = Math.max(200, nodeX) + PADDING; const labelWidth = (nodeX + nodeWidth) - PADDING - labelX; if (labelWidth < 20) return makeMap(); @@ -38,9 +40,11 @@ const getPositionedLabels = (nodes, transform) => { class NodesResourcesLayer extends React.Component { render() { - const { transform, nodes, labels } = this.props; + const { yPosition, topologyId, transform, nodes, labels } = this.props; + const height = RESOURCES_LAYER_HEIGHT * transform.scaleY; + return ( - + {nodes.map(node => ( ))} + + {topologyId} + ); } } function mapStateToProps(state, props) { + const yPosition = layersVerticalPositionSelector(state).get(props.topologyId); const nodes = layoutNodesByTopologyMetaSelector(state)(state)[props.topologyId]; // TODO: Move to selectors? const labels = getPositionedLabels(nodes, props.transform); - return { nodes, labels }; + return { yPosition, nodes, labels }; } export default connect( diff --git a/client/app/scripts/charts/nodes-resources.js b/client/app/scripts/charts/nodes-resources.js index e812156b73..db28f4214a 100644 --- a/client/app/scripts/charts/nodes-resources.js +++ b/client/app/scripts/charts/nodes-resources.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import Logo from '../components/logo'; -import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; +import { layersTopologyIdsSelector } from '../selectors/resource-view/layers'; import CachableZoomWrapper from '../components/cachable-zoom-wrapper'; import NodesResourcesLayer from './nodes-resources-layer'; diff --git a/client/app/scripts/components/view-mode-selector.js b/client/app/scripts/components/view-mode-selector.js index 66b32889f6..6d8f43b0d4 100644 --- a/client/app/scripts/components/view-mode-selector.js +++ b/client/app/scripts/components/view-mode-selector.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import classNames from 'classnames'; import { setGraphView, setTableView, setResourceView } from '../actions/app-actions'; -import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; +import { layersTopologyIdsSelector } from '../selectors/resource-view/layers'; import { isGraphViewModeSelector, isTableViewModeSelector, diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index 260a8d0758..bf9d73b28e 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -16,7 +16,9 @@ export const CANVAS_MARGINS = { bottom: 0, }; +export const RESOURCES_LAYER_TITLE_WIDTH = 200; export const RESOURCES_LAYER_HEIGHT = 150; +export const RESOURCES_LAYER_PADDING = 10; // Node shapes export const NODE_SHAPE_HIGHLIGHT_RADIUS = 70; diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index ee633e5eca..90e5e61d0c 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -1,27 +1,29 @@ import { createSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; -import { CANVAS_MARGINS } from '../../constants/styles'; +import { CANVAS_MARGINS, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; import { viewportWidthSelector, viewportHeightSelector } from '../viewport'; +import { layersVerticalPositionSelector } from './layers'; import { layoutNodesSelector } from './layout'; // Compute the default zoom settings for the given chart. export const resourcesDefaultZoomSelector = createSelector( [ + layersVerticalPositionSelector, layoutNodesSelector, viewportWidthSelector, viewportHeightSelector, ], - (layoutNodes, width, height) => { + (layersVerticalPositions, layoutNodes, width, height) => { if (layoutNodes.size === 0) { return makeMap(); } const xMin = layoutNodes.map(n => n.get('x')).min(); - const yMin = layoutNodes.map(n => n.get('y')).min(); + const yMin = layersVerticalPositions.toList().min(); const xMax = layoutNodes.map(n => n.get('x') + n.get('width')).max(); - const yMax = layoutNodes.map(n => n.get('y') + n.get('height')).max(); + const yMax = layersVerticalPositions.toList().max() + RESOURCES_LAYER_HEIGHT; const scaleX = (width / (xMax - xMin)); const scaleY = (height / (yMax - yMin)); diff --git a/client/app/scripts/selectors/resource-view/layer-factory.js b/client/app/scripts/selectors/resource-view/layer-factory.js index cd3f32486d..5eb1c648bc 100644 --- a/client/app/scripts/selectors/resource-view/layer-factory.js +++ b/client/app/scripts/selectors/resource-view/layer-factory.js @@ -1,6 +1,7 @@ import { createSelector, createStructuredSelector } from 'reselect'; import { fromJS, Map as makeMap } from 'immutable'; +import { layersVerticalPositionSelector } from './layers'; import { layersDefs, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; import { getNodeColor } from '../../utils/color-utils'; /* eslint no-unused-vars: 0 */ @@ -16,9 +17,10 @@ export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) createSelector( [ state => state.getIn(['nodesByTopology', topologyId], makeMap()), + layersVerticalPositionSelector, parentLayerNodesSelector, ], - (nodes, parentLayerNodes) => { + (nodes, layersVerticalPosition, parentLayerNodes) => { const childrenXOffset = { [basePseudoId]: 0 }; const layerDef = layersDefs[topologyId]; let positionedNodes = makeMap(); @@ -49,7 +51,7 @@ export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) childrenXOffset[parentId] = childrenXOffset[parentId] || parentLayerNodes.getIn([parentId, 'x'], 0); const nodeX = childrenXOffset[parentId]; - const nodeY = parentLayerNodes.getIn([parentId, 'y'], 0) - RESOURCES_LAYER_HEIGHT; + const nodeY = layersVerticalPosition.get(topologyId); // console.log(nodeX, parentId); // TODO: Remove. diff --git a/client/app/scripts/selectors/resource-view/layers.js b/client/app/scripts/selectors/resource-view/layers.js new file mode 100644 index 0000000000..1432d568ea --- /dev/null +++ b/client/app/scripts/selectors/resource-view/layers.js @@ -0,0 +1,29 @@ +import { createSelector } from 'reselect'; +import { Map as makeMap, fromJS } from 'immutable'; + +import { resourceViewLayers } from '../../constants/resources'; +import { RESOURCES_LAYER_PADDING, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; + +export const layersTopologyIdsSelector = createSelector( + [ + state => state.get('currentTopologyId'), + ], + topologyId => fromJS(resourceViewLayers[topologyId] || []) +); + +export const layersVerticalPositionSelector = createSelector( + [ + layersTopologyIdsSelector, + ], + (topologiesIds) => { + let yPositions = makeMap(); + let currentY = RESOURCES_LAYER_PADDING; + + topologiesIds.forEach((topologyId) => { + currentY -= RESOURCES_LAYER_HEIGHT + RESOURCES_LAYER_PADDING; + yPositions = yPositions.set(topologyId, currentY); + }); + + return yPositions; + } +); diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js index f3621362db..0f25cec603 100644 --- a/client/app/scripts/selectors/resource-view/layout.js +++ b/client/app/scripts/selectors/resource-view/layout.js @@ -1,17 +1,9 @@ import { createSelector, createStructuredSelector } from 'reselect'; -import { fromJS } from 'immutable'; +import { layersTopologyIdsSelector } from './layers'; import { layerNodesSelectorFactory } from './layer-factory'; -import { resourceViewLayers } from '../../constants/resources'; -export const layersTopologyIdsSelector = createSelector( - [ - state => state.get('currentTopologyId'), - ], - topologyId => fromJS(resourceViewLayers[topologyId] || []) -); - export const layoutNodesByTopologyMetaSelector = createSelector( [ layersTopologyIdsSelector, diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 192840d787..dccb2a1bc7 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -946,6 +946,17 @@ font-size: 15px; padding: 5px; } + + &-layer .layer-name { + background-color: #eee; + border: 1px solid #ccc; + color: #aaa; + font-size: 20px; + font-weight: bold; + padding-right: 20px; + text-align: right; + text-transform: uppercase; + } } // This part sets the styles only for the 'real' node details table, not applying From 8ac6d8a54cc3fa889fdbee41bfeaceb4898687aa Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Sun, 12 Mar 2017 00:03:37 +0100 Subject: [PATCH 17/32] Made metric selectors work for the resource view --- .../scripts/charts/nodes-resources-layer.js | 4 +- client/app/scripts/components/app.js | 10 ++-- .../components/cachable-zoom-wrapper.js | 2 +- .../components/metric-selector-item.js | 6 +-- .../app/scripts/components/metric-selector.js | 6 ++- .../scripts/components/view-mode-selector.js | 2 + .../selectors/resource-view/layer-factory.js | 14 +++-- client/app/scripts/selectors/topology.js | 51 ++++++++++--------- client/app/styles/_base.scss | 2 +- 9 files changed, 57 insertions(+), 40 deletions(-) diff --git a/client/app/scripts/charts/nodes-resources-layer.js b/client/app/scripts/charts/nodes-resources-layer.js index fbafd1bdf8..2ac727033a 100644 --- a/client/app/scripts/charts/nodes-resources-layer.js +++ b/client/app/scripts/charts/nodes-resources-layer.js @@ -46,7 +46,7 @@ class NodesResourcesLayer extends React.Component { return ( - {nodes.map(node => ( + {nodes.toIndexedSeq().map(node => ( - {labels.map(label => ( + {labels.toIndexedSeq().map(label => ( - {showingMetricsSelector && !isTableViewMode && } + {showingMetricsSelector && isGraphViewMode && } {showingNetworkSelector && isGraphViewMode && } - + {!isResourceViewMode && } @@ -152,6 +153,7 @@ class App extends React.Component { function mapStateToProps(state) { return { activeTopologyOptions: activeTopologyOptionsSelector(state), + isResourceViewMode: isResourceViewModeSelector(state), isTableViewMode: isTableViewModeSelector(state), isGraphViewMode: isGraphViewModeSelector(state), routeSet: state.get('routeSet'), diff --git a/client/app/scripts/components/cachable-zoom-wrapper.js b/client/app/scripts/components/cachable-zoom-wrapper.js index 7d9698eb5a..592e14ae29 100644 --- a/client/app/scripts/components/cachable-zoom-wrapper.js +++ b/client/app/scripts/components/cachable-zoom-wrapper.js @@ -118,7 +118,7 @@ class CachableZoomWrapper extends React.Component { // Restore the zooming settings this.zoom = this.zoom .scaleExtent([zoomState.minScale, zoomState.maxScale]) - .extent([[50, 50], [200, 600]]) + // .extent([[50, 50], [200, 600]]) .translateExtent([ [zoomState.minTranslateX, zoomState.minTranslateY], [zoomState.maxTranslateX, zoomState.maxTranslateY], diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js index 3bce457ec6..c1d2d5aab0 100644 --- a/client/app/scripts/components/metric-selector-item.js +++ b/client/app/scripts/components/metric-selector-item.js @@ -22,10 +22,10 @@ class MetricSelectorItem extends React.Component { const k = this.props.metric.get('id'); const pinnedMetric = this.props.pinnedMetric; - if (k === pinnedMetric) { - this.props.unpinMetric(k); - } else { + if (k !== pinnedMetric) { this.props.pinMetric(k); + } else if (!this.props.alwaysPinned) { + this.props.unpinMetric(k); } } diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index 619a52f2c7..34435249e7 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -20,7 +20,11 @@ class MetricSelector extends React.Component {
{this.props.availableCanvasMetrics.map(metric => ( - + ))}
diff --git a/client/app/scripts/components/view-mode-selector.js b/client/app/scripts/components/view-mode-selector.js index 6d8f43b0d4..3e505ad5de 100644 --- a/client/app/scripts/components/view-mode-selector.js +++ b/client/app/scripts/components/view-mode-selector.js @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import classNames from 'classnames'; +import MetricSelector from './metric-selector'; import { setGraphView, setTableView, setResourceView } from '../actions/app-actions'; import { layersTopologyIdsSelector } from '../selectors/resource-view/layers'; import { @@ -43,6 +44,7 @@ class ViewModeSelector extends React.Component { {hasResourceView && Item('fa fa-bar-chart', 'Resources', isResourceViewMode, this.props.setResourceView)}
+ {isResourceViewMode && }
); } diff --git a/client/app/scripts/selectors/resource-view/layer-factory.js b/client/app/scripts/selectors/resource-view/layer-factory.js index 5eb1c648bc..f17766a76a 100644 --- a/client/app/scripts/selectors/resource-view/layer-factory.js +++ b/client/app/scripts/selectors/resource-view/layer-factory.js @@ -11,16 +11,19 @@ import { getNodeColor } from '../../utils/color-utils'; const basePseudoId = 'base'; // TODO: Make this variable -const getCPUMetric = node => (node.get('metrics') || makeMap()).find(m => m.get('label') === 'CPU'); +const getMetric = (node, metricName) => ( + node.get('metrics', makeMap()).find(m => m.get('label') === metricName) +); export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) => ( createSelector( [ state => state.getIn(['nodesByTopology', topologyId], makeMap()), + state => state.get('pinnedMetricType', 'CPU'), layersVerticalPositionSelector, parentLayerNodesSelector, ], - (nodes, layersVerticalPosition, parentLayerNodes) => { + (nodes, pinnedMetricType, layersVerticalPosition, parentLayerNodes) => { const childrenXOffset = { [basePseudoId]: 0 }; const layerDef = layersDefs[topologyId]; let positionedNodes = makeMap(); @@ -28,14 +31,15 @@ export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) parentLayerNodes = parentLayerNodes || makeMap({ basePseudoId: makeMap({ x: 0 }) }); nodes.forEach((node) => { - const metric = getCPUMetric(node); + const metric = getMetric(node, pinnedMetricType); if (!metric) return; const nodeId = node.get('id'); const nodeColor = getNodeColor(node.get('rank'), node.get('label'), node.get('pseudo')); - const totalCapacity = metric.get('max'); - const absoluteConsumption = metric.get('value') / (topologyId === 'processes' ? 4 : 1); + const totalCapacity = metric.get('max') / 1e5; + const absoluteConsumption = metric.get('value') / 1e5 + / (topologyId === 'processes' ? 4 : 1); const relativeConsumption = absoluteConsumption / totalCapacity; const nodeConsumption = layerDef.withCapacity ? relativeConsumption : 1; diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index 0990e90380..6393914ec9 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -3,6 +3,27 @@ import { RESOURCE_VIEW_MODE, GRAPH_VIEW_MODE, TABLE_VIEW_MODE } from '../constan // TODO: Consider moving more stuff from 'topology-utils' here. +export const isGraphViewModeSelector = createSelector( + [ + state => state.get('topologyViewMode'), + ], + viewMode => viewMode === GRAPH_VIEW_MODE +); + +export const isTableViewModeSelector = createSelector( + [ + state => state.get('topologyViewMode'), + ], + viewMode => viewMode === TABLE_VIEW_MODE +); + +export const isResourceViewModeSelector = createSelector( + [ + state => state.get('topologyViewMode'), + ], + viewMode => viewMode === RESOURCE_VIEW_MODE +); + // Checks if graph complexity is high. Used to trigger // table view on page load and decide on animations. export const graphExceedsComplexityThreshSelector = createSelector( @@ -27,31 +48,15 @@ export const activeTopologyOptionsSelector = createSelector( export const activeTopologyZoomCacheKeyPathSelector = createSelector( [ + isGraphViewModeSelector, state => state.get('topologyViewMode'), state => state.get('currentTopologyId'), + state => state.get('pinnedMetricType'), state => JSON.stringify(activeTopologyOptionsSelector(state)), ], - (viewMode, topologyId, topologyOptions) => ['zoomCache', viewMode, topologyId, topologyOptions] -); - - -export const isGraphViewModeSelector = createSelector( - [ - state => state.get('topologyViewMode'), - ], - viewMode => viewMode === GRAPH_VIEW_MODE -); - -export const isTableViewModeSelector = createSelector( - [ - state => state.get('topologyViewMode'), - ], - viewMode => viewMode === TABLE_VIEW_MODE -); - -export const isResourceViewModeSelector = createSelector( - [ - state => state.get('topologyViewMode'), - ], - viewMode => viewMode === RESOURCE_VIEW_MODE + (isGraphViewMode, viewMode, topologyId, pinnedMetricType, topologyOptions) => ( + isGraphViewMode ? + ['zoomCache', viewMode, topologyId, topologyOptions] : + ['zoomCache', viewMode, topologyId, pinnedMetricType] + ) ); diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index dccb2a1bc7..b380ce756f 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -1289,7 +1289,7 @@ } } -.view-mode-selector .fa { +.view-mode-selector-wrapper .fa { margin-right: 4px; margin-left: 0; color: $text-secondary-color; From bfd3271b4133215c6f25ef96f89172811290c0fa Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Sun, 12 Mar 2017 18:12:48 +0100 Subject: [PATCH 18/32] Adjusted the viewport selectors --- client/app/scripts/charts/nodes-grid.js | 14 ++++++++------ client/app/scripts/components/help-panel.js | 5 +++-- client/app/scripts/components/nodes.js | 8 ++------ client/app/scripts/constants/styles.js | 9 +++++---- .../selectors/graph-view/default-zoom.js | 11 ++++++----- .../selectors/resource-view/default-zoom.js | 11 ++++++----- client/app/scripts/selectors/viewport.js | 19 +++++++++++++++---- client/app/styles/_base.scss | 2 +- 8 files changed, 46 insertions(+), 33 deletions(-) diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js index 1512d142d9..5251b36413 100644 --- a/client/app/scripts/charts/nodes-grid.js +++ b/client/app/scripts/charts/nodes-grid.js @@ -7,8 +7,8 @@ import NodeDetailsTable from '../components/node-details/node-details-table'; import { clickNode, sortOrderChanged } from '../actions/app-actions'; import { shownNodesSelector } from '../selectors/node-filters'; -import { CANVAS_MARGINS } from '../constants/styles'; import { searchNodeMatchesSelector } from '../selectors/search'; +import { canvasMarginsSelector } from '../selectors/viewport'; import { getNodeColor } from '../utils/color-utils'; @@ -97,13 +97,13 @@ class NodesGrid extends React.Component { } render() { - const { nodes, height, gridSortedBy, gridSortedDesc, + const { nodes, height, gridSortedBy, gridSortedDesc, canvasMargins, searchNodeMatches, searchQuery } = this.props; const cmpStyle = { height, - marginTop: CANVAS_MARGINS.top, - paddingLeft: CANVAS_MARGINS.left, - paddingRight: CANVAS_MARGINS.right, + marginTop: canvasMargins.top, + paddingLeft: canvasMargins.left, + paddingRight: canvasMargins.right, }; const tbodyHeight = height - 24 - 18; const className = 'scroll-body'; @@ -146,6 +146,7 @@ class NodesGrid extends React.Component { function mapStateToProps(state) { return { nodes: shownNodesSelector(state), + canvasMargins: canvasMarginsSelector(state), gridSortedBy: state.get('gridSortedBy'), gridSortedDesc: state.get('gridSortedDesc'), currentTopology: state.get('currentTopology'), @@ -153,7 +154,8 @@ function mapStateToProps(state) { searchNodeMatches: searchNodeMatchesSelector(state), searchQuery: state.get('searchQuery'), selectedNodeId: state.get('selectedNodeId'), - height: state.getIn(['viewport', 'height']), + // TODO: Change this. + height: state.getIn(['viewport', 'height']) - 190, }; } diff --git a/client/app/scripts/components/help-panel.js b/client/app/scripts/components/help-panel.js index d672ed4203..a9d54cfaed 100644 --- a/client/app/scripts/components/help-panel.js +++ b/client/app/scripts/components/help-panel.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { searchableFieldsSelector } from '../selectors/search'; -import { CANVAS_MARGINS } from '../constants/styles'; +import { canvasMarginsSelector } from '../selectors/viewport'; import { hideHelp } from '../actions/app-actions'; @@ -152,7 +152,7 @@ function renderFieldsPanel(currentTopologyName, searchableFields) { function HelpPanel({currentTopologyName, searchableFields, onClickClose}) { return (
-
+

Help

@@ -176,6 +176,7 @@ function HelpPanel({currentTopologyName, searchableFields, onClickClose}) { function mapStateToProps(state) { return { + canvasMargins: canvasMarginsSelector(state), searchableFields: searchableFieldsSelector(state), currentTopologyName: state.getIn(['currentTopology', 'fullName']) }; diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js index 9c8e44dbaf..f588a5c8b0 100644 --- a/client/app/scripts/components/nodes.js +++ b/client/app/scripts/components/nodes.js @@ -15,11 +15,9 @@ import { isTableViewModeSelector, isResourceViewModeSelector, } from '../selectors/topology'; -import { VIEWPORT_RESIZE_DEBOUNCE_INTERVAL } from '../constants/timer'; +import { VIEWPORT_RESIZE_DEBOUNCE_INTERVAL } from '../constants/timer'; -const navbarHeight = 194; -const marginTop = 0; const EmptyTopologyError = show => (
diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js index 5251b36413..47f3d46e5b 100644 --- a/client/app/scripts/charts/nodes-grid.js +++ b/client/app/scripts/charts/nodes-grid.js @@ -7,8 +7,8 @@ import NodeDetailsTable from '../components/node-details/node-details-table'; import { clickNode, sortOrderChanged } from '../actions/app-actions'; import { shownNodesSelector } from '../selectors/node-filters'; +import { canvasMarginsSelector, canvasHeightSelector } from '../selectors/canvas'; import { searchNodeMatchesSelector } from '../selectors/search'; -import { canvasMarginsSelector } from '../selectors/viewport'; import { getNodeColor } from '../utils/color-utils'; @@ -147,6 +147,7 @@ function mapStateToProps(state) { return { nodes: shownNodesSelector(state), canvasMargins: canvasMarginsSelector(state), + height: canvasHeightSelector(state), gridSortedBy: state.get('gridSortedBy'), gridSortedDesc: state.get('gridSortedDesc'), currentTopology: state.get('currentTopology'), @@ -154,8 +155,6 @@ function mapStateToProps(state) { searchNodeMatches: searchNodeMatchesSelector(state), searchQuery: state.get('searchQuery'), selectedNodeId: state.get('selectedNodeId'), - // TODO: Change this. - height: state.getIn(['viewport', 'height']) - 190, }; } diff --git a/client/app/scripts/charts/nodes-resources-layer.js b/client/app/scripts/charts/nodes-resources-layer.js index 2ac727033a..55ec7b51ff 100644 --- a/client/app/scripts/charts/nodes-resources-layer.js +++ b/client/app/scripts/charts/nodes-resources-layer.js @@ -72,13 +72,13 @@ class NodesResourcesLayer extends React.Component { /> ))} - {topologyId} - + } ); } diff --git a/client/app/scripts/charts/nodes-resources.js b/client/app/scripts/charts/nodes-resources.js index db28f4214a..e8513aebfc 100644 --- a/client/app/scripts/charts/nodes-resources.js +++ b/client/app/scripts/charts/nodes-resources.js @@ -25,7 +25,7 @@ class NodesResources extends React.Component { width="100%" height="100%" id="nodes-chart-canvas"> - + {transform => this.renderLayers(transform)} diff --git a/client/app/scripts/components/cachable-zoom-wrapper.js b/client/app/scripts/components/cachable-zoom-wrapper.js index 592e14ae29..e0f6668d05 100644 --- a/client/app/scripts/components/cachable-zoom-wrapper.js +++ b/client/app/scripts/components/cachable-zoom-wrapper.js @@ -9,6 +9,11 @@ import { zoom, zoomIdentity } from 'd3-zoom'; import { cacheZoomState } from '../actions/app-actions'; import { activeLayoutZoomSelector } from '../selectors/zooming'; import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/topology'; +import { + canvasMarginsSelector, + canvasWidthSelector, + canvasHeightSelector, +} from '../selectors/canvas'; import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer'; @@ -116,13 +121,20 @@ class CachableZoomWrapper extends React.Component { const zoomState = props.layoutZoom.toJS(); // Restore the zooming settings - this.zoom = this.zoom - .scaleExtent([zoomState.minScale, zoomState.maxScale]) - // .extent([[50, 50], [200, 600]]) - .translateExtent([ - [zoomState.minTranslateX, zoomState.minTranslateY], - [zoomState.maxTranslateX, zoomState.maxTranslateY], - ]); + this.zoom = this.zoom.scaleExtent([zoomState.minScale, zoomState.maxScale]); + + if (props.bounded) { + this.zoom = this.zoom + .translateExtent([ + [zoomState.minTranslateX, zoomState.minTranslateY], + [zoomState.maxTranslateX, zoomState.maxTranslateY], + ]) + .extent([ + [props.canvasMargins.left, props.canvasMargins.top], + [props.canvasMargins.left + props.width, props.canvasMargins.top + props.height] + ]); + } + this.svg.call(this.zoom.transform, zoomIdentity .translate(zoomState.translateX, zoomState.translateY) .scale(zoomState.scaleX, zoomState.scaleY)); @@ -151,6 +163,9 @@ class CachableZoomWrapper extends React.Component { function mapStateToProps(state) { return { + width: canvasWidthSelector(state), + height: canvasHeightSelector(state), + canvasMargins: canvasMarginsSelector(state), layoutZoom: activeLayoutZoomSelector(state), layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)), forceRelayout: state.get('forceRelayout'), diff --git a/client/app/scripts/components/help-panel.js b/client/app/scripts/components/help-panel.js index a9d54cfaed..e20e9132ff 100644 --- a/client/app/scripts/components/help-panel.js +++ b/client/app/scripts/components/help-panel.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { searchableFieldsSelector } from '../selectors/search'; -import { canvasMarginsSelector } from '../selectors/viewport'; +import { canvasMarginsSelector } from '../selectors/canvas'; import { hideHelp } from '../actions/app-actions'; diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index f6bc01160c..2a6adcf54e 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -2,21 +2,13 @@ import { GRAPH_VIEW_MODE, TABLE_VIEW_MODE, RESOURCE_VIEW_MODE } from './naming'; export const DETAILS_PANEL_WIDTH = 420; - +export const DETAILS_PANEL_OFFSET = 8; export const DETAILS_PANEL_MARGINS = { top: 24, bottom: 48, right: 36 }; -export const DETAILS_PANEL_OFFSET = 8; - -export const CANVAS_MARGINS = { - [GRAPH_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 150 }, - [TABLE_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 150 }, - [RESOURCE_VIEW_MODE]: { top: 200, left: 40, right: 40, bottom: 100 }, -}; - export const RESOURCES_LAYER_TITLE_WIDTH = 200; export const RESOURCES_LAYER_HEIGHT = 150; export const RESOURCES_LAYER_PADDING = 10; @@ -35,6 +27,13 @@ export const NODE_SHAPE_DOT_RADIUS = 10; // are given on a small unit scale as foreign objects in SVG. export const NODE_BASE_SIZE = 100; + +export const CANVAS_MARGINS = { + [GRAPH_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 150 }, + [TABLE_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 30 }, + [RESOURCE_VIEW_MODE]: { top: 160, left: 210, right: 40, bottom: 50 }, +}; + // Node details table constants export const NODE_DETAILS_TABLE_CW = { XS: '32px', @@ -79,44 +78,15 @@ export const NODE_DETAILS_TABLE_XS_LABEL = { container: '#', }; -// TODO: Make this variable -export const resourcesLayers = [{ - topologyId: 'hosts', - horizontalPadding: 0, - verticalPadding: 10, - frameHeight: 200, - withCapacity: true, -}, { - topologyId: 'containers', - horizontalPadding: 0, - verticalPadding: 7.5, - frameHeight: 150, - withCapacity: false, -}, { - topologyId: 'processes', - horizontalPadding: 0, - verticalPadding: 5, - frameHeight: 100, - withCapacity: false, -}]; export const layersDefs = { hosts: { - parentTopologyId: null, - verticalPadding: 10, - frameHeight: 200, withCapacity: true, }, containers: { - parentTopologyId: 'hosts', - verticalPadding: 7.5, - frameHeight: 150, withCapacity: false, }, processes: { - parentTopologyId: 'containers', - verticalPadding: 5, - frameHeight: 100, withCapacity: false, }, }; diff --git a/client/app/scripts/selectors/viewport.js b/client/app/scripts/selectors/canvas.js similarity index 67% rename from client/app/scripts/selectors/viewport.js rename to client/app/scripts/selectors/canvas.js index 15ea581d2b..4cd864d3c3 100644 --- a/client/app/scripts/selectors/viewport.js +++ b/client/app/scripts/selectors/canvas.js @@ -14,7 +14,7 @@ export const canvasMarginsSelector = createSelector( viewMode => CANVAS_MARGINS[viewMode] || { top: 0, left: 0, right: 0, bottom: 0 } ); -export const viewportWidthSelector = createSelector( +export const canvasWidthSelector = createSelector( [ state => state.getIn(['viewport', 'width']), canvasMarginsSelector, @@ -22,7 +22,7 @@ export const viewportWidthSelector = createSelector( (width, margins) => width - margins.left - margins.right ); -export const viewportHeightSelector = createSelector( +export const canvasHeightSelector = createSelector( [ state => state.getIn(['viewport', 'height']), canvasMarginsSelector, @@ -30,34 +30,34 @@ export const viewportHeightSelector = createSelector( (height, margins) => height - margins.top - margins.bottom ); -const viewportFocusWidthSelector = createSelector( +const canvasWithDetailsWidthSelector = createSelector( [ - viewportWidthSelector, + canvasWidthSelector, ], width => width - DETAILS_PANEL_WIDTH - DETAILS_PANEL_MARGINS.right ); -export const viewportFocusHorizontalCenterSelector = createSelector( +export const canvasDetailsHorizontalCenterSelector = createSelector( [ - viewportFocusWidthSelector, + canvasWithDetailsWidthSelector, canvasMarginsSelector, ], (width, margins) => (width / 2) + margins.left ); -export const viewportFocusVerticalCenterSelector = createSelector( +export const canvasDetailsVerticalCenterSelector = createSelector( [ - viewportHeightSelector, + canvasHeightSelector, canvasMarginsSelector, ], (height, margins) => (height / 2) + margins.top ); // The narrower dimension of the viewport, used for the circular layout. -export const viewportCircularExpanseSelector = createSelector( +export const canvasCircularExpanseSelector = createSelector( [ - viewportFocusWidthSelector, - viewportHeightSelector, + canvasWithDetailsWidthSelector, + canvasHeightSelector, ], (width, height) => Math.min(width, height) ); diff --git a/client/app/scripts/selectors/graph-view/default-zoom.js b/client/app/scripts/selectors/graph-view/default-zoom.js index 3a243746d4..b0dd34696d 100644 --- a/client/app/scripts/selectors/graph-view/default-zoom.js +++ b/client/app/scripts/selectors/graph-view/default-zoom.js @@ -2,19 +2,17 @@ import { createSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; import { NODE_BASE_SIZE } from '../../constants/styles'; -import { canvasMarginsSelector, viewportWidthSelector, viewportHeightSelector } from '../viewport'; +import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from '../canvas'; import { graphNodesSelector } from './graph'; -const MARGIN_FACTOR = 10; - // Compute the default zoom settings for the given graph. export const graphDefaultZoomSelector = createSelector( [ graphNodesSelector, canvasMarginsSelector, - viewportWidthSelector, - viewportHeightSelector, + canvasWidthSelector, + canvasHeightSelector, ], (graphNodes, canvasMargins, width, height) => { if (graphNodes.size === 0) { @@ -43,14 +41,7 @@ export const graphDefaultZoomSelector = createSelector( const translateX = ((width - ((xMax + xMin) * scale)) / 2) + canvasMargins.left; const translateY = ((height - ((yMax + yMin) * scale)) / 2) + canvasMargins.top; - const xMargin = (xMax - xMin) * MARGIN_FACTOR; - const yMargin = (yMax - yMin) * MARGIN_FACTOR; - return makeMap({ - minTranslateX: xMin - xMargin, - maxTranslateX: xMax + xMargin, - minTranslateY: yMin - yMargin, - maxTranslateY: yMax + yMargin, translateX, translateY, minScale, diff --git a/client/app/scripts/selectors/graph-view/graph.js b/client/app/scripts/selectors/graph-view/graph.js index 3927048650..37c9c74f57 100644 --- a/client/app/scripts/selectors/graph-view/graph.js +++ b/client/app/scripts/selectors/graph-view/graph.js @@ -3,7 +3,7 @@ import { createSelector, createStructuredSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; import { initEdgesFromNodes } from '../../utils/layouter-utils'; -import { viewportWidthSelector, viewportHeightSelector } from './viewport'; +import { canvasWidthSelector, canvasHeightSelector } from '../canvas'; import { activeTopologyOptionsSelector } from '../topology'; import { shownNodesSelector } from '../node-filters'; import { doLayout } from '../../charts/nodes-layout'; @@ -16,8 +16,8 @@ const layoutOptionsSelector = createStructuredSelector({ forceRelayout: state => state.get('forceRelayout'), topologyId: state => state.get('currentTopologyId'), topologyOptions: activeTopologyOptionsSelector, - height: viewportHeightSelector, - width: viewportWidthSelector, + height: canvasHeightSelector, + width: canvasWidthSelector, }); const graphLayoutSelector = createSelector( diff --git a/client/app/scripts/selectors/graph-view/layout.js b/client/app/scripts/selectors/graph-view/layout.js index a90a021610..52172f79ba 100644 --- a/client/app/scripts/selectors/graph-view/layout.js +++ b/client/app/scripts/selectors/graph-view/layout.js @@ -7,10 +7,10 @@ import { NODE_BASE_SIZE } from '../../constants/styles'; import { graphNodesSelector, graphEdgesSelector } from './graph'; import { activeLayoutZoomSelector } from '../zooming'; import { - viewportCircularExpanseSelector, - viewportFocusHorizontalCenterSelector, - viewportFocusVerticalCenterSelector, -} from '../viewport'; + canvasCircularExpanseSelector, + canvasDetailsHorizontalCenterSelector, + canvasDetailsVerticalCenterSelector, +} from '../canvas'; const circularOffsetAngle = Math.PI / 4; @@ -23,8 +23,8 @@ const radiusDensity = scaleThreshold() const translationToViewportCenterSelector = createSelector( [ - viewportFocusHorizontalCenterSelector, - viewportFocusVerticalCenterSelector, + canvasDetailsHorizontalCenterSelector, + canvasDetailsVerticalCenterSelector, activeLayoutZoomSelector, ], (centerX, centerY, zoomState) => { @@ -78,7 +78,7 @@ const circularLayoutScalarsSelector = createSelector( // TODO: Fix this. state => activeLayoutZoomSelector(state).get('scaleX'), state => focusedNodesIdsSelector(state).length - 1, - viewportCircularExpanseSelector, + canvasCircularExpanseSelector, ], (scale, circularNodesCount, viewportExpanse) => { // Here we calculate the zoom factor of the nodes that get selected into focus. diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index 03f60719be..d72050c71a 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -2,7 +2,7 @@ import { createSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; import { RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; -import { canvasMarginsSelector, viewportWidthSelector, viewportHeightSelector } from '../viewport'; +import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from '../canvas'; import { layersVerticalPositionSelector } from './layers'; import { layoutNodesSelector } from './layout'; @@ -13,8 +13,8 @@ export const resourcesDefaultZoomSelector = createSelector( layersVerticalPositionSelector, layoutNodesSelector, canvasMarginsSelector, - viewportWidthSelector, - viewportHeightSelector, + canvasWidthSelector, + canvasHeightSelector, ], (layersVerticalPositions, layoutNodes, canvasMargins, width, height) => { if (layoutNodes.size === 0) { @@ -22,13 +22,15 @@ export const resourcesDefaultZoomSelector = createSelector( } const xMin = layoutNodes.map(n => n.get('x')).min(); - const yMin = layersVerticalPositions.toList().min(); + const yMin = layersVerticalPositions.min(); const xMax = layoutNodes.map(n => n.get('x') + n.get('width')).max(); - const yMax = layersVerticalPositions.toList().max() + RESOURCES_LAYER_HEIGHT; + const yMax = layersVerticalPositions.max() + RESOURCES_LAYER_HEIGHT; - const scaleX = (width / (xMax - xMin)); - const scaleY = (height / (yMax - yMin)); - const maxScale = scaleX * 2000; + const minNodeWidth = layoutNodes.map(n => n.get('width')).min(); + + const scaleX = (width / (xMax - xMin)) * 1.0; + const scaleY = (height / (yMax - yMin)) * 0.7; + const maxScale = width / minNodeWidth; const minScale = scaleX; // This translation puts the graph in the center of the viewport, respecting the margins. diff --git a/client/app/scripts/selectors/resource-view/layer-factory.js b/client/app/scripts/selectors/resource-view/layer-factory.js index f17766a76a..fa6d5b5c55 100644 --- a/client/app/scripts/selectors/resource-view/layer-factory.js +++ b/client/app/scripts/selectors/resource-view/layer-factory.js @@ -15,7 +15,8 @@ const getMetric = (node, metricName) => ( node.get('metrics', makeMap()).find(m => m.get('label') === metricName) ); -export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) => ( +export const layerNodesSelectorFactory = + (topologyId, parentTopologyId, parentLayerNodesSelector) => ( createSelector( [ state => state.getIn(['nodesByTopology', topologyId], makeMap()), @@ -46,7 +47,7 @@ export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) const nodeWidth = layerDef.withCapacity ? totalCapacity : absoluteConsumption; const parents = node.get('parents') || makeMap(); - const parent = parents.find(p => p.get('topologyId') === layerDef.parentTopologyId); + const parent = parents.find(p => p.get('topologyId') === parentTopologyId); const parentId = parent ? parent.get('id') : basePseudoId; // NOTE: We don't handle uncontained yet. @@ -64,6 +65,7 @@ export const layerNodesSelectorFactory = (topologyId, parentLayerNodesSelector) childrenXOffset[parentId] += nodeWidth; positionedNodes = positionedNodes.set(nodeId, node.merge(makeMap({ + topologyId, color: nodeColor, x: nodeX, y: nodeY, diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js index 0f25cec603..d9ac9f1283 100644 --- a/client/app/scripts/selectors/resource-view/layout.js +++ b/client/app/scripts/selectors/resource-view/layout.js @@ -11,10 +11,12 @@ export const layoutNodesByTopologyMetaSelector = createSelector( (layersTopologyIds) => { const layerSelectorsMap = {}; let prevSelector = () => null; + let prevTopId = null; layersTopologyIds.forEach((topId) => { - layerSelectorsMap[topId] = layerNodesSelectorFactory(topId, prevSelector); + layerSelectorsMap[topId] = layerNodesSelectorFactory(topId, prevTopId, prevSelector); prevSelector = layerSelectorsMap[topId]; + prevTopId = topId; }); return createStructuredSelector(layerSelectorsMap); diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 1e5de05dea..5349c1a496 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -951,7 +951,7 @@ background-color: rgba(#eee, 0.95); border: 1px solid #ccc; color: #aaa; - font-size: 20px; + font-size: 18px; font-weight: bold; padding-right: 20px; text-align: right; From feb461cf7fb01f6f1c414866d7bb7f8ba010a50c Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 14 Mar 2017 20:55:05 +0100 Subject: [PATCH 20/32] Showing more useful metric info in the resource box labels --- .../app/scripts/charts/node-resource-box.js | 7 +- .../scripts/charts/nodes-resources-layer.js | 11 ++- client/app/scripts/charts/nodes-resources.js | 3 +- client/app/scripts/decorators/node.js | 44 +++++++++ .../selectors/resource-view/default-zoom.js | 20 ++-- .../selectors/resource-view/layer-factory.js | 84 ---------------- .../scripts/selectors/resource-view/layers.js | 99 ++++++++++++++++++- .../scripts/selectors/resource-view/layout.js | 34 ------- 8 files changed, 163 insertions(+), 139 deletions(-) create mode 100644 client/app/scripts/decorators/node.js delete mode 100644 client/app/scripts/selectors/resource-view/layer-factory.js delete mode 100644 client/app/scripts/selectors/resource-view/layout.js diff --git a/client/app/scripts/charts/node-resource-box.js b/client/app/scripts/charts/node-resource-box.js index 4cc10f36c9..b7942cb05e 100644 --- a/client/app/scripts/charts/node-resource-box.js +++ b/client/app/scripts/charts/node-resource-box.js @@ -16,8 +16,9 @@ export default class NodeResourceBox extends React.Component { } render() { - const { info, color, width, height, x, y, consumption } = this.props; - const innerHeight = height * consumption; + const { color, width, height, x, y, activeMetric } = this.props; + const { relativeConsumption, info } = activeMetric.toJS(); + const innerHeight = height * relativeConsumption; return ( @@ -42,7 +43,7 @@ export default class NodeResourceBox extends React.Component { height={innerHeight} width={width} x={x} - y={y + (height * (1 - consumption))} + y={y + (height * (1 - relativeConsumption))} /> ); diff --git a/client/app/scripts/charts/nodes-resources-layer.js b/client/app/scripts/charts/nodes-resources-layer.js index 55ec7b51ff..5e6fa3042a 100644 --- a/client/app/scripts/charts/nodes-resources-layer.js +++ b/client/app/scripts/charts/nodes-resources-layer.js @@ -3,8 +3,10 @@ import { connect } from 'react-redux'; import { Map as makeMap } from 'immutable'; import { RESOURCES_LAYER_TITLE_WIDTH, RESOURCES_LAYER_HEIGHT } from '../constants/styles'; -import { layoutNodesByTopologyMetaSelector } from '../selectors/resource-view/layout'; -import { layersVerticalPositionSelector } from '../selectors/resource-view/layers'; +import { + layersVerticalPositionSelector, + positionedNodesByTopologySelector, +} from '../selectors/resource-view/layers'; import NodeResourceBox from './node-resource-box'; import NodeResourceLabel from './node-resource-label'; @@ -52,11 +54,10 @@ class NodesResourcesLayer extends React.Component { color={node.get('color')} width={node.get('width')} height={node.get('height')} - consumption={node.get('consumption')} withCapacity={node.get('withCapacity')} x={node.get('x')} y={node.get('y')} - info={node.get('info')} + activeMetric={node.get('activeMetric')} meta={node.get('meta')} /> ))} @@ -86,7 +87,7 @@ class NodesResourcesLayer extends React.Component { function mapStateToProps(state, props) { const yPosition = layersVerticalPositionSelector(state).get(props.topologyId); - const nodes = layoutNodesByTopologyMetaSelector(state)(state)[props.topologyId]; + const nodes = positionedNodesByTopologySelector(state).get(props.topologyId, makeMap()); // TODO: Move to selectors? const labels = getPositionedLabels(nodes, props.transform); return { yPosition, nodes, labels }; diff --git a/client/app/scripts/charts/nodes-resources.js b/client/app/scripts/charts/nodes-resources.js index e8513aebfc..474dfc92fa 100644 --- a/client/app/scripts/charts/nodes-resources.js +++ b/client/app/scripts/charts/nodes-resources.js @@ -9,11 +9,12 @@ import NodesResourcesLayer from './nodes-resources-layer'; class NodesResources extends React.Component { renderLayers(transform) { - return this.props.layersTopologyIds.map(topologyId => ( + return this.props.layersTopologyIds.map((topologyId, index) => ( )); } diff --git a/client/app/scripts/decorators/node.js b/client/app/scripts/decorators/node.js new file mode 100644 index 0000000000..78fa2564b7 --- /dev/null +++ b/client/app/scripts/decorators/node.js @@ -0,0 +1,44 @@ +import { Map as makeMap } from 'immutable'; + +import { getNodeColor } from '../utils/color-utils'; +import { getMetricValue } from '../utils/metric-utils'; +import { RESOURCES_LAYER_HEIGHT } from '../constants/styles'; + + +export function nodeColorDecorator(node) { + return node.set('color', getNodeColor(node.get('rank'), node.get('label'), node.get('pseudo'))); +} + +export function nodeActiveMetricDecorator(node) { + const metricType = node.get('activeMetricType'); + const metric = node.get('metrics', makeMap()).find(m => m.get('label') === metricType); + if (!metric) return node; + + const { formattedValue } = getMetricValue(metric); + const info = `${metricType} - ${formattedValue}`; + const absoluteConsumption = metric.get('value'); + const withCapacity = node.get('withCapacity'); + const totalCapacity = withCapacity ? metric.get('max') : absoluteConsumption; + const relativeConsumption = absoluteConsumption / totalCapacity; + + return node.set('activeMetric', makeMap({ + totalCapacity, absoluteConsumption, relativeConsumption, info + })); +} + +export function nodeResourceBoxDecorator(node) { + const widthCriterion = node.get('withCapacity') ? 'totalCapacity' : 'absoluteConsumption'; + const width = node.getIn(['activeMetric', widthCriterion]) * 1e-5; + const height = RESOURCES_LAYER_HEIGHT; + + return node.merge(makeMap({ width, height })); +} + +export function nodeParentNodeDecorator(node) { + const parentTopologyId = node.get('directParentTopologyId'); + const parents = node.get('parents', makeMap()); + const parent = parents.find(p => p.get('topologyId') === parentTopologyId); + if (!parent) return node; + + return node.set('parentNodeId', parent.get('id')); +} diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index d72050c71a..6c81362aab 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -3,30 +3,30 @@ import { Map as makeMap } from 'immutable'; import { RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from '../canvas'; -import { layersVerticalPositionSelector } from './layers'; -import { layoutNodesSelector } from './layout'; +import { layersVerticalPositionSelector, positionedNodesByTopologySelector } from './layers'; // Compute the default zoom settings for the given chart. export const resourcesDefaultZoomSelector = createSelector( [ layersVerticalPositionSelector, - layoutNodesSelector, + positionedNodesByTopologySelector, canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector, ], - (layersVerticalPositions, layoutNodes, canvasMargins, width, height) => { - if (layoutNodes.size === 0) { + (verticalPositions, nodes, canvasMargins, width, height) => { + if (nodes.size === 0) { return makeMap(); } - const xMin = layoutNodes.map(n => n.get('x')).min(); - const yMin = layersVerticalPositions.min(); - const xMax = layoutNodes.map(n => n.get('x') + n.get('width')).max(); - const yMax = layersVerticalPositions.max() + RESOURCES_LAYER_HEIGHT; + const flattenedNodes = nodes.flatten(true); + const xMin = flattenedNodes.map(n => n.get('x')).min(); + const yMin = verticalPositions.toList().min(); + const xMax = flattenedNodes.map(n => n.get('x') + n.get('width')).max(); + const yMax = verticalPositions.toList().max() + RESOURCES_LAYER_HEIGHT; - const minNodeWidth = layoutNodes.map(n => n.get('width')).min(); + const minNodeWidth = flattenedNodes.map(n => n.get('width')).min(); const scaleX = (width / (xMax - xMin)) * 1.0; const scaleY = (height / (yMax - yMin)) * 0.7; diff --git a/client/app/scripts/selectors/resource-view/layer-factory.js b/client/app/scripts/selectors/resource-view/layer-factory.js deleted file mode 100644 index fa6d5b5c55..0000000000 --- a/client/app/scripts/selectors/resource-view/layer-factory.js +++ /dev/null @@ -1,84 +0,0 @@ -import { createSelector, createStructuredSelector } from 'reselect'; -import { fromJS, Map as makeMap } from 'immutable'; - -import { layersVerticalPositionSelector } from './layers'; -import { layersDefs, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; -import { getNodeColor } from '../../utils/color-utils'; -/* eslint no-unused-vars: 0 */ -/* eslint no-nested-ternary: 0 */ -/* eslint no-sequences: 0 */ - -const basePseudoId = 'base'; - -// TODO: Make this variable -const getMetric = (node, metricName) => ( - node.get('metrics', makeMap()).find(m => m.get('label') === metricName) -); - -export const layerNodesSelectorFactory = - (topologyId, parentTopologyId, parentLayerNodesSelector) => ( - createSelector( - [ - state => state.getIn(['nodesByTopology', topologyId], makeMap()), - state => state.get('pinnedMetricType', 'CPU'), - layersVerticalPositionSelector, - parentLayerNodesSelector, - ], - (nodes, pinnedMetricType, layersVerticalPosition, parentLayerNodes) => { - const childrenXOffset = { [basePseudoId]: 0 }; - const layerDef = layersDefs[topologyId]; - let positionedNodes = makeMap(); - - parentLayerNodes = parentLayerNodes || makeMap({ basePseudoId: makeMap({ x: 0 }) }); - - nodes.forEach((node) => { - const metric = getMetric(node, pinnedMetricType); - if (!metric) return; - - const nodeId = node.get('id'); - const nodeColor = getNodeColor(node.get('rank'), node.get('label'), node.get('pseudo')); - - const totalCapacity = metric.get('max') / 1e5; - const absoluteConsumption = metric.get('value') / 1e5 - / (topologyId === 'processes' ? 4 : 1); - const relativeConsumption = absoluteConsumption / totalCapacity; - const nodeConsumption = layerDef.withCapacity ? relativeConsumption : 1; - - const nodeWidth = layerDef.withCapacity ? totalCapacity : absoluteConsumption; - - const parents = node.get('parents') || makeMap(); - const parent = parents.find(p => p.get('topologyId') === parentTopologyId); - const parentId = parent ? parent.get('id') : basePseudoId; - - // NOTE: We don't handle uncontained yet. - if (parentId === basePseudoId && topologyId !== 'hosts') return; - - childrenXOffset[parentId] = childrenXOffset[parentId] - || parentLayerNodes.getIn([parentId, 'x'], 0); - const nodeX = childrenXOffset[parentId]; - const nodeY = layersVerticalPosition.get(topologyId); - - // console.log(nodeX, parentId); - // TODO: Remove. - if (nodeX === undefined) return; - - childrenXOffset[parentId] += nodeWidth; - - positionedNodes = positionedNodes.set(nodeId, node.merge(makeMap({ - topologyId, - color: nodeColor, - x: nodeX, - y: nodeY, - width: nodeWidth, - height: RESOURCES_LAYER_HEIGHT, - consumption: nodeConsumption, - withCapacity: layerDef.withCapacity, - info: `CPU usage: ${absoluteConsumption}%`, - meta: node, - }))); - }); - - return positionedNodes; - } - ) -); diff --git a/client/app/scripts/selectors/resource-view/layers.js b/client/app/scripts/selectors/resource-view/layers.js index 1432d568ea..40e35c22f5 100644 --- a/client/app/scripts/selectors/resource-view/layers.js +++ b/client/app/scripts/selectors/resource-view/layers.js @@ -1,8 +1,24 @@ +import { times } from 'lodash'; +import { fromJS, Map as makeMap } from 'immutable'; import { createSelector } from 'reselect'; -import { Map as makeMap, fromJS } from 'immutable'; -import { resourceViewLayers } from '../../constants/resources'; import { RESOURCES_LAYER_PADDING, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; +import { resourceViewLayers } from '../../constants/resources'; +import { + nodeColorDecorator, + nodeParentNodeDecorator, + nodeResourceBoxDecorator, + nodeActiveMetricDecorator, +} from '../../decorators/node'; + + +const RESOURCE_VIEW_MAX_LAYERS = 3; + +const nodeWeight = node => ( + node.get('withCapacity') ? + -node.getIn(['activeMetric', 'relativeConsumption']) : + -node.get('width') +); export const layersTopologyIdsSelector = createSelector( [ @@ -27,3 +43,82 @@ export const layersVerticalPositionSelector = createSelector( return yPositions; } ); + +const decoratedNodesByTopologySelector = createSelector( + [ + layersTopologyIdsSelector, + state => state.get('pinnedMetricType'), + ...times(RESOURCE_VIEW_MAX_LAYERS, index => ( + state => state.getIn(['nodesByTopology', layersTopologyIdsSelector(state).get(index)]) + )) + ], + (layersTopologyIds, pinnedMetricType, ...topologiesNodes) => { + let nodesByTopology = makeMap(); + let lastLayerTopologyId = null; + + topologiesNodes.forEach((topologyNodes, index) => { + const layerTopologyId = layersTopologyIds.get(index); + const decoratedTopologyNodes = (topologyNodes || makeMap()) + .map(node => node.set('directParentTopologyId', lastLayerTopologyId)) + .map(node => node.set('topologyId', layerTopologyId)) + .map(node => node.set('activeMetricType', pinnedMetricType)) + .map(node => node.set('withCapacity', layerTopologyId === 'hosts')) + .map(nodeActiveMetricDecorator) + .map(nodeResourceBoxDecorator) + .map(nodeParentNodeDecorator) + .map(nodeColorDecorator); + const filteredTopologyNodes = decoratedTopologyNodes + .map(node => node.set('meta', node)) + .filter(node => node.get('parentNodeId') || index === 0) + .filter(node => node.get('width')); + + nodesByTopology = nodesByTopology.set(layerTopologyId, filteredTopologyNodes); + lastLayerTopologyId = layerTopologyId; + }); + + return nodesByTopology; + } +); + +export const positionedNodesByTopologySelector = createSelector( + [ + layersTopologyIdsSelector, + decoratedNodesByTopologySelector, + layersVerticalPositionSelector, + ], + (layersTopologyIds, decoratedNodesByTopology, layersVerticalPosition) => { + let result = makeMap(); + + layersTopologyIds.forEach((layerTopologyId, index) => { + const decoratedNodes = decoratedNodesByTopology.get(layerTopologyId, makeMap()); + const buckets = decoratedNodes.groupBy(node => node.get('parentNodeId')); + const y = layersVerticalPosition.get(layerTopologyId); + + buckets.forEach((bucket, parentNodeId) => { + const parentTopologyId = layersTopologyIds.get(index - 1); + let x = result.getIn([parentTopologyId, parentNodeId, 'x'], 0); + + bucket.sortBy(nodeWeight).forEach((node, nodeId) => { + const positionedNode = node.merge(makeMap({ x, y })); + result = result.setIn([layerTopologyId, nodeId], positionedNode); + x += node.get('width'); + }); + + const offset = result.getIn([parentTopologyId, parentNodeId, 'x'], 0); + const overhead = (x - offset) / result.getIn([parentTopologyId, parentNodeId, 'width'], x); + if (overhead > 1) { + console.log(overhead); + bucket.forEach((_, nodeId) => { + const node = result.getIn([layerTopologyId, nodeId]); + result = result.mergeIn([layerTopologyId, nodeId], makeMap({ + x: ((node.get('x') - offset) / overhead) + offset, + width: node.get('width') / overhead, + })); + }); + } + }); + }); + + return result; + } +); diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js deleted file mode 100644 index d9ac9f1283..0000000000 --- a/client/app/scripts/selectors/resource-view/layout.js +++ /dev/null @@ -1,34 +0,0 @@ -import { createSelector, createStructuredSelector } from 'reselect'; - -import { layersTopologyIdsSelector } from './layers'; -import { layerNodesSelectorFactory } from './layer-factory'; - - -export const layoutNodesByTopologyMetaSelector = createSelector( - [ - layersTopologyIdsSelector, - ], - (layersTopologyIds) => { - const layerSelectorsMap = {}; - let prevSelector = () => null; - let prevTopId = null; - - layersTopologyIds.forEach((topId) => { - layerSelectorsMap[topId] = layerNodesSelectorFactory(topId, prevTopId, prevSelector); - prevSelector = layerSelectorsMap[topId]; - prevTopId = topId; - }); - - return createStructuredSelector(layerSelectorsMap); - } -); - -export const layoutNodesSelector = createSelector( - [ - state => layoutNodesByTopologyMetaSelector(state)(state), - layersTopologyIdsSelector, - ], - (layoutNodesByTopology, layersTopologyIds) => ( - layersTopologyIds.flatMap(topId => layoutNodesByTopology[topId].toList()) - ) -); From c273da294f60251d926a0b40043efa8af1377c4d Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 15 Mar 2017 12:55:40 +0100 Subject: [PATCH 21/32] Fetching only necessary nodes for the resource view --- client/app/scripts/actions/app-actions.js | 28 ++++++++++++++------- client/app/scripts/reducers/root.js | 16 +++++++++--- client/app/scripts/utils/web-api-utils.js | 30 +++++++++++++++++------ 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 602569f2c7..7ff8ab9b35 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -13,6 +13,7 @@ import { import { doControlRequest, getAllNodes, + getResourceViewNodesSnapshot, getNodesDelta, getNodeDetails, getTopologies, @@ -23,8 +24,15 @@ import { import { getCurrentTopologyUrl } from '../utils/topology-utils'; import { storageSet } from '../utils/storage-utils'; import { loadTheme } from '../utils/contrast-utils'; -import { activeTopologyOptionsSelector } from '../selectors/topology'; -import { RESOURCE_VIEW_MODE, GRAPH_VIEW_MODE, TABLE_VIEW_MODE } from '../constants/naming'; +import { + activeTopologyOptionsSelector, + isResourceViewModeSelector, +} from '../selectors/topology'; +import { + GRAPH_VIEW_MODE, + TABLE_VIEW_MODE, + RESOURCE_VIEW_MODE, + } from '../constants/naming'; const log = debug('scope:app-actions'); @@ -282,9 +290,8 @@ export function setResourceView() { viewMode: RESOURCE_VIEW_MODE, }); updateRoute(getState); - setTimeout(() => { - getAllNodes(getState, dispatch); - }, 1200); + // Update the nodes for all topologies that appear in the current resource view. + getResourceViewNodesSnapshot(getState, dispatch); }; } @@ -721,10 +728,13 @@ export function route(urlState) { state.get('nodeDetails'), dispatch ); - // TODO: Remove this - setTimeout(() => { - getAllNodes(getState, dispatch); - }, 1200); + // If we are landing on the resource view page, we need to fetch not only all the + // nodes for the current topology, but also the nodes of all the topologies that make + // the layers in the resource view. + if (isResourceViewModeSelector(state)) { + // Get all the nodes for the current resource view layout in the next run-cycle. + setTimeout(() => { getResourceViewNodesSnapshot(getState, dispatch); }, 0); + } }; } diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 4f011bb266..818745604b 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -5,7 +5,12 @@ import { fromJS, is as isDeepEqual, List as makeList, Map as makeMap, OrderedMap as makeOrderedMap, Set as makeSet } from 'immutable'; import ActionTypes from '../constants/action-types'; -import { EDGE_ID_SEPARATOR, GRAPH_VIEW_MODE, TABLE_VIEW_MODE } from '../constants/naming'; +import { + EDGE_ID_SEPARATOR, + GRAPH_VIEW_MODE, + TABLE_VIEW_MODE, + RESOURCE_VIEW_MODE, +} from '../constants/naming'; import { graphExceedsComplexityThreshSelector, activeTopologyZoomCacheKeyPathSelector, @@ -53,7 +58,7 @@ export const initialState = makeMap({ nodeDetails: makeOrderedMap(), // nodeId -> details nodes: makeOrderedMap(), // nodeId -> node nodesLoaded: false, - // nodes cache, infrequently updated, used for search + // nodes cache, infrequently updated, used for search & resource view nodesByTopology: makeMap(), // topologyId -> nodes pinnedMetric: null, // class of metric, e.g. 'cpu', rather than 'host_cpu' or 'process_cpu'. @@ -621,7 +626,12 @@ export function rootReducer(state = initialState, action) { } // update nodes cache - return state.setIn(['nodesByTopology', state.get('currentTopologyId')], state.get('nodes')); + if (state.get('topologyViewMode') !== RESOURCE_VIEW_MODE) { + state = state.setIn( + ['nodesByTopology', state.get('currentTopologyId')], state.get('nodes')); + } + + return state; } case ActionTypes.RECEIVE_NODES_FOR_TOPOLOGY: { diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 6fa2331c02..dd118cc625 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -2,6 +2,7 @@ import debug from 'debug'; import reqwest from 'reqwest'; import trimStart from 'lodash/trimStart'; import defaults from 'lodash/defaults'; +import { Map as makeMap } from 'immutable'; import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveError, receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError, @@ -9,6 +10,7 @@ import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveEr receiveControlSuccess, receiveTopologies, receiveNotFound, receiveNodesForTopology } from '../actions/app-actions'; +import { layersTopologyIdsSelector } from '../selectors/resource-view/layers'; import { API_INTERVAL, TOPOLOGY_INTERVAL } from '../constants/timer'; const log = debug('scope:web-api-utils'); @@ -156,14 +158,10 @@ function doRequest(opts) { return reqwest(config); } -/** - * Gets nodes for all topologies (for search) - */ -export function getAllNodes(getState, dispatch) { - const state = getState(); - const topologyOptions = state.get('topologyOptions'); +function getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions = makeMap()) { // fetch sequentially - state.get('topologyUrlsById') + getState().get('topologyUrlsById') + .filter((_, topologyId) => topologyIds.contains(topologyId)) .reduce((sequence, topologyUrl, topologyId) => sequence.then(() => { const optionsQuery = buildOptionsQuery(topologyOptions.get(topologyId)); // Trim the leading slash from the url before requesting. @@ -175,6 +173,24 @@ export function getAllNodes(getState, dispatch) { Promise.resolve()); } +/** + * Gets nodes for all topologies (for search) + */ +export function getAllNodes(getState, dispatch) { + const state = getState(); + const topologyIds = state.get('topologies').map(topology => topology.get('id')); + const topologyOptions = state.get('topologyOptions'); + getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions); +} + +// NOTE: At the moment we are only getting their one-time snapshot (instead of polling), +// because we intentionally want to keep the resource view layout static. Later on, we +// will probably want to change this. +export function getResourceViewNodesSnapshot(getState, dispatch) { + const topologyIds = layersTopologyIdsSelector(getState()); + getNodesForTopologies(getState, dispatch, topologyIds); +} + export function getTopologies(options, dispatch, initialPoll) { // Used to resume polling when navigating between pages in Weave Cloud. continuePolling = initialPoll === true ? true : continuePolling; From a57f043f84ccbab3459db93d1ce73979e65310ca Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 15 Mar 2017 16:55:18 +0100 Subject: [PATCH 22/32] Refactored the resource view layer component --- .../scripts/charts/layer-labels-overlay.js | 54 +++++++++++++ .../app/scripts/charts/layer-topology-name.js | 22 ++++++ .../app/scripts/charts/node-resource-box.js | 65 +++++++--------- .../scripts/charts/nodes-resources-layer.js | 78 +++++-------------- client/app/scripts/constants/styles.js | 3 + .../selectors/resource-view/default-zoom.js | 4 +- .../scripts/selectors/resource-view/layers.js | 36 ++++----- client/app/scripts/utils/transform-utils.js | 3 + client/app/styles/_base.scss | 4 +- 9 files changed, 152 insertions(+), 117 deletions(-) create mode 100644 client/app/scripts/charts/layer-labels-overlay.js create mode 100644 client/app/scripts/charts/layer-topology-name.js create mode 100644 client/app/scripts/utils/transform-utils.js diff --git a/client/app/scripts/charts/layer-labels-overlay.js b/client/app/scripts/charts/layer-labels-overlay.js new file mode 100644 index 0000000000..21c20ecbc1 --- /dev/null +++ b/client/app/scripts/charts/layer-labels-overlay.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { fromJS } from 'immutable'; + +import NodeResourceLabel from './node-resource-label'; +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({ + id: node.get('id'), + label: node.get('label'), + width: width - (2 * RESOURCES_LABEL_PADDING), + x: xTrimmed + RESOURCES_LABEL_PADDING, + y: y + RESOURCES_LABEL_PADDING, + }); + } + }); + + return fromJS(labels); + } + + render() { + return ( + + {this.positionedLabels().map(label => ( + + ))} + + ); + } +} diff --git a/client/app/scripts/charts/layer-topology-name.js b/client/app/scripts/charts/layer-topology-name.js new file mode 100644 index 0000000000..d8db91801a --- /dev/null +++ b/client/app/scripts/charts/layer-topology-name.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import { RESOURCES_LAYER_TITLE_WIDTH, RESOURCES_LAYER_HEIGHT } from '../constants/styles'; +import { applyTransformY } from '../utils/transform-utils'; + +export default class LayerTopologyName extends React.Component { + render() { + const { verticalOffset, topologyId, transform } = this.props; + const height = RESOURCES_LAYER_HEIGHT * transform.scaleY; + const y = applyTransformY(transform, verticalOffset); + + return ( + + {topologyId} + + ); + } +} diff --git a/client/app/scripts/charts/node-resource-box.js b/client/app/scripts/charts/node-resource-box.js index b7942cb05e..ece91a7d18 100644 --- a/client/app/scripts/charts/node-resource-box.js +++ b/client/app/scripts/charts/node-resource-box.js @@ -1,51 +1,42 @@ import React from 'react'; +import { connect } from 'react-redux'; -const frameFill = 'rgba(100, 100, 100, 0.3)'; -const frameStroke = 'rgba(255, 255, 255, 1)'; -const frameStrokeWidth = 1.5; -export default class NodeResourceBox extends React.Component { - constructor(props, context) { - super(props, context); - - this.handleMouseClick = this.handleMouseClick.bind(this); - } - - handleMouseClick() { - console.log(this.props.meta.toJS()); +class NodeResourceBox extends React.Component { + defaultRectProps(relativeHeight = 1) { + const stroke = this.props.contrastMode ? 'black' : 'white'; + const translateY = this.props.height * (1 - relativeHeight); + return { + transform: `translate(0, ${translateY})`, + height: this.props.height * relativeHeight, + width: this.props.width, + x: this.props.x, + y: this.props.y, + vectorEffect: 'non-scaling-stroke', + strokeWidth: 1, + stroke, + }; } render() { - const { color, width, height, x, y, activeMetric } = this.props; + const { color, withCapacity, activeMetric } = this.props; const { relativeConsumption, info } = activeMetric.toJS(); - const innerHeight = height * relativeConsumption; + const frameFill = 'rgba(150, 150, 150, 0.4)'; return ( - + {info} - - + {withCapacity && } + ); } } + +function mapStateToProps(state) { + return { + contrastMode: state.get('contrastMode') + }; +} + +export default connect(mapStateToProps)(NodeResourceBox); diff --git a/client/app/scripts/charts/nodes-resources-layer.js b/client/app/scripts/charts/nodes-resources-layer.js index 5e6fa3042a..11618a3101 100644 --- a/client/app/scripts/charts/nodes-resources-layer.js +++ b/client/app/scripts/charts/nodes-resources-layer.js @@ -2,48 +2,22 @@ import React from 'react'; import { connect } from 'react-redux'; import { Map as makeMap } from 'immutable'; -import { RESOURCES_LAYER_TITLE_WIDTH, RESOURCES_LAYER_HEIGHT } from '../constants/styles'; +import NodeResourceBox from './node-resource-box'; +import LayerLabelsOverlay from './layer-labels-overlay'; +import LayerTopologyName from './layer-topology-name'; import { layersVerticalPositionSelector, positionedNodesByTopologySelector, } from '../selectors/resource-view/layers'; -import NodeResourceBox from './node-resource-box'; -import NodeResourceLabel from './node-resource-label'; - -const PADDING = 10; -const stringifiedTransform = ({ scaleX, scaleY, translateX, translateY }) => ( +const stringifiedTransform = ({ scaleX = 1, scaleY = 1, translateX = 0, translateY = 0 }) => ( `translate(${translateX},${translateY}) scale(${scaleX},${scaleY})` ); -const getPositionedLabels = (nodes, transform) => { - const { scaleX, scaleY, translateX, translateY } = transform; - return nodes.map((node) => { - const nodeX = (node.get('x') * scaleX) + translateX; - const nodeY = (node.get('y') * scaleY) + translateY; - const nodeWidth = node.get('width') * scaleX; - - const labelY = nodeY + PADDING; - const labelX = Math.max(200, nodeX) + PADDING; - const labelWidth = (nodeX + nodeWidth) - PADDING - labelX; - - if (labelWidth < 20) return makeMap(); - - return makeMap({ - id: node.get('id'), - label: node.get('label'), - width: labelWidth, - x: labelX, - y: labelY, - }); - }).filter(label => !label.isEmpty()); -}; - class NodesResourcesLayer extends React.Component { render() { - const { yPosition, topologyId, transform, nodes, labels } = this.props; - const height = RESOURCES_LAYER_HEIGHT * transform.scaleY; + const { layerVerticalPosition, topologyId, transform, nodes } = this.props; return ( @@ -55,42 +29,32 @@ class NodesResourcesLayer extends React.Component { width={node.get('width')} height={node.get('height')} withCapacity={node.get('withCapacity')} - x={node.get('x')} - y={node.get('y')} activeMetric={node.get('activeMetric')} - meta={node.get('meta')} - /> - ))} - - - {labels.toIndexedSeq().map(label => ( - ))} - {!nodes.isEmpty() && - {topologyId} - } + + {!nodes.isEmpty() && } ); } } function mapStateToProps(state, props) { - const yPosition = layersVerticalPositionSelector(state).get(props.topologyId); - const nodes = positionedNodesByTopologySelector(state).get(props.topologyId, makeMap()); - // TODO: Move to selectors? - const labels = getPositionedLabels(nodes, props.transform); - return { yPosition, nodes, labels }; + return { + layerVerticalPosition: layersVerticalPositionSelector(state).get(props.topologyId), + nodes: positionedNodesByTopologySelector(state).get(props.topologyId, makeMap()) + }; } export default connect( diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index 2a6adcf54e..3327ae3c9b 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -9,9 +9,12 @@ export const DETAILS_PANEL_MARGINS = { right: 36 }; +// Resource view export const RESOURCES_LAYER_TITLE_WIDTH = 200; export const RESOURCES_LAYER_HEIGHT = 150; export const RESOURCES_LAYER_PADDING = 10; +export const RESOURCES_LABEL_MIN_SIZE = 50; +export const RESOURCES_LABEL_PADDING = 10; // Node shapes export const NODE_SHAPE_HIGHLIGHT_RADIUS = 70; diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index 6c81362aab..9dff5f3778 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -21,9 +21,9 @@ export const resourcesDefaultZoomSelector = createSelector( } const flattenedNodes = nodes.flatten(true); - const xMin = flattenedNodes.map(n => n.get('x')).min(); + const xMin = flattenedNodes.map(n => n.get('offset')).min(); const yMin = verticalPositions.toList().min(); - const xMax = flattenedNodes.map(n => n.get('x') + n.get('width')).max(); + const xMax = flattenedNodes.map(n => n.get('offset') + n.get('width')).max(); const yMax = verticalPositions.toList().max() + RESOURCES_LAYER_HEIGHT; const minNodeWidth = flattenedNodes.map(n => n.get('width')).min(); diff --git a/client/app/scripts/selectors/resource-view/layers.js b/client/app/scripts/selectors/resource-view/layers.js index 40e35c22f5..dd5f127b9f 100644 --- a/client/app/scripts/selectors/resource-view/layers.js +++ b/client/app/scripts/selectors/resource-view/layers.js @@ -68,7 +68,6 @@ const decoratedNodesByTopologySelector = createSelector( .map(nodeParentNodeDecorator) .map(nodeColorDecorator); const filteredTopologyNodes = decoratedTopologyNodes - .map(node => node.set('meta', node)) .filter(node => node.get('parentNodeId') || index === 0) .filter(node => node.get('width')); @@ -84,38 +83,37 @@ export const positionedNodesByTopologySelector = createSelector( [ layersTopologyIdsSelector, decoratedNodesByTopologySelector, - layersVerticalPositionSelector, ], - (layersTopologyIds, decoratedNodesByTopology, layersVerticalPosition) => { + (layersTopologyIds, decoratedNodesByTopology) => { let result = makeMap(); layersTopologyIds.forEach((layerTopologyId, index) => { const decoratedNodes = decoratedNodesByTopology.get(layerTopologyId, makeMap()); const buckets = decoratedNodes.groupBy(node => node.get('parentNodeId')); - const y = layersVerticalPosition.get(layerTopologyId); buckets.forEach((bucket, parentNodeId) => { const parentTopologyId = layersTopologyIds.get(index - 1); - let x = result.getIn([parentTopologyId, parentNodeId, 'x'], 0); + let offset = result.getIn([parentTopologyId, parentNodeId, 'offset'], 0); bucket.sortBy(nodeWeight).forEach((node, nodeId) => { - const positionedNode = node.merge(makeMap({ x, y })); + const positionedNode = node.set('offset', offset); result = result.setIn([layerTopologyId, nodeId], positionedNode); - x += node.get('width'); + offset += node.get('width'); }); - const offset = result.getIn([parentTopologyId, parentNodeId, 'x'], 0); - const overhead = (x - offset) / result.getIn([parentTopologyId, parentNodeId, 'width'], x); - if (overhead > 1) { - console.log(overhead); - bucket.forEach((_, nodeId) => { - const node = result.getIn([layerTopologyId, nodeId]); - result = result.mergeIn([layerTopologyId, nodeId], makeMap({ - x: ((node.get('x') - offset) / overhead) + offset, - width: node.get('width') / overhead, - })); - }); - } + // const offset = result.getIn([parentTopologyId, parentNodeId, 'x'], 0); + // const overhead = + // (x - offset) / result.getIn([parentTopologyId, parentNodeId, 'width'], x); + // if (overhead > 1) { + // console.log(overhead); + // bucket.forEach((_, nodeId) => { + // const node = result.getIn([layerTopologyId, nodeId]); + // result = result.mergeIn([layerTopologyId, nodeId], makeMap({ + // x: ((node.get('x') - offset) / overhead) + offset, + // width: node.get('width') / overhead, + // })); + // }); + // } }); }); diff --git a/client/app/scripts/utils/transform-utils.js b/client/app/scripts/utils/transform-utils.js new file mode 100644 index 0000000000..a4bdfd39f6 --- /dev/null +++ b/client/app/scripts/utils/transform-utils.js @@ -0,0 +1,3 @@ + +export const applyTransformX = ({ scaleX = 1, translateX = 0 }, x) => (x * scaleX) + translateX; +export const applyTransformY = ({ scaleY = 1, translateY = 0 }, y) => (y * scaleY) + translateY; diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 5349c1a496..8b0abf8c14 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -947,11 +947,11 @@ padding: 5px; } - &-layer .layer-name { + &-layer .layer-topology-name { background-color: rgba(#eee, 0.95); border: 1px solid #ccc; color: #aaa; - font-size: 18px; + font-size: 16px; font-weight: bold; padding-right: 20px; text-align: right; From b3eb612392b047d7fa1cec86eb6d50a3098957ae Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Thu, 16 Mar 2017 17:06:24 +0100 Subject: [PATCH 23/32] Addressed first batch UI comments (from the Scope meeting) --- .../scripts/charts/layer-labels-overlay.js | 11 ++++---- .../app/scripts/charts/node-resource-box.js | 1 + .../app/scripts/charts/node-resource-info.js | 17 ++++++++++++ .../app/scripts/charts/node-resource-label.js | 23 ---------------- client/app/scripts/components/app.js | 2 +- .../scripts/components/view-mode-selector.js | 11 ++++---- client/app/scripts/constants/resources.js | 7 +++-- client/app/scripts/decorators/node.js | 9 ++++--- .../scripts/selectors/resource-view/layers.js | 11 ++++---- client/app/scripts/utils/metric-utils.js | 20 +++++++++++++- client/app/styles/_base.scss | 27 ++++++++++++------- client/app/styles/_contrast-overrides.scss | 1 + client/app/styles/_variables.scss | 1 + 13 files changed, 86 insertions(+), 55 deletions(-) create mode 100644 client/app/scripts/charts/node-resource-info.js delete mode 100644 client/app/scripts/charts/node-resource-label.js diff --git a/client/app/scripts/charts/layer-labels-overlay.js b/client/app/scripts/charts/layer-labels-overlay.js index 21c20ecbc1..1163d570b8 100644 --- a/client/app/scripts/charts/layer-labels-overlay.js +++ b/client/app/scripts/charts/layer-labels-overlay.js @@ -1,7 +1,7 @@ import React from 'react'; import { fromJS } from 'immutable'; -import NodeResourceLabel from './node-resource-label'; +import NodeResourceInfo from './node-resource-info'; import { applyTransformX, applyTransformY } from '../utils/transform-utils'; import { RESOURCES_LAYER_TITLE_WIDTH, @@ -24,11 +24,10 @@ export default class LayerLabelsOverlay extends React.Component { if (width >= RESOURCES_LABEL_MIN_SIZE) { labels.push({ - id: node.get('id'), - label: node.get('label'), width: width - (2 * RESOURCES_LABEL_PADDING), x: xTrimmed + RESOURCES_LABEL_PADDING, y: y + RESOURCES_LABEL_PADDING, + node, }); } }); @@ -40,9 +39,9 @@ export default class LayerLabelsOverlay extends React.Component { return ( {this.positionedLabels().map(label => ( - + {node.get('label')} + {humanizedMetricInfo} + + ); + } +} diff --git a/client/app/scripts/charts/node-resource-label.js b/client/app/scripts/charts/node-resource-label.js deleted file mode 100644 index 7d10e61d2c..0000000000 --- a/client/app/scripts/charts/node-resource-label.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - - -export default class NodeResourceBox extends React.Component { - render() { - const { label, width, x, y } = this.props; - - return ( - - -
- {label} -
-
-
- ); - } -} diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 73f807c5fa..1ea2871ae8 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -140,7 +140,7 @@ class App extends React.Component { {showingMetricsSelector && isGraphViewMode && } {showingNetworkSelector && isGraphViewMode && } {!isResourceViewMode && } - + {!isResourceViewMode && }
diff --git a/client/app/scripts/components/view-mode-selector.js b/client/app/scripts/components/view-mode-selector.js index 3e505ad5de..332b7026f8 100644 --- a/client/app/scripts/components/view-mode-selector.js +++ b/client/app/scripts/components/view-mode-selector.js @@ -12,14 +12,15 @@ import { } from '../selectors/topology'; -const Item = (icons, label, isSelected, onClick) => { +const Item = (icons, label, isSelected, onClick, isEnabled = true) => { const className = classNames('view-mode-selector-action', { - 'view-mode-selector-action-selected': isSelected + 'view-mode-selector-action-selected': isSelected, }); return (
+ disabled={!isEnabled} + onClick={isEnabled && onClick}> {label}
@@ -41,8 +42,8 @@ class ViewModeSelector extends React.Component {
{Item('fa fa-share-alt', 'Graph', isGraphViewMode, this.props.setGraphView)} {Item('fa fa-table', 'Table', isTableViewMode, this.props.setTableView)} - {hasResourceView && - Item('fa fa-bar-chart', 'Resources', isResourceViewMode, this.props.setResourceView)} + {Item('fa fa-bar-chart', 'Resources', isResourceViewMode, this.props.setResourceView, + hasResourceView)}
{isResourceViewMode && }
diff --git a/client/app/scripts/constants/resources.js b/client/app/scripts/constants/resources.js index 6eb8a657d4..0b96e435ea 100644 --- a/client/app/scripts/constants/resources.js +++ b/client/app/scripts/constants/resources.js @@ -1,5 +1,8 @@ -// TODO: Consider fetching this from the backend. +// TODO: Consider fetching these from the backend. +export const topologiesWithCapacity = ['hosts']; export const resourceViewLayers = { - hosts: ['hosts', 'containers', 'processes'] + hosts: ['hosts', 'containers', 'processes'], + containers: ['containers', 'processes'], + processes: ['processes'], }; diff --git a/client/app/scripts/decorators/node.js b/client/app/scripts/decorators/node.js index 78fa2564b7..49cf0a4fa7 100644 --- a/client/app/scripts/decorators/node.js +++ b/client/app/scripts/decorators/node.js @@ -5,8 +5,10 @@ import { getMetricValue } from '../utils/metric-utils'; import { RESOURCES_LAYER_HEIGHT } from '../constants/styles'; -export function nodeColorDecorator(node) { - return node.set('color', getNodeColor(node.get('rank'), node.get('label'), node.get('pseudo'))); +export function nodeResourceViewColorDecorator(node) { + // Color lightness is normally determined from the node label. However, in the resource view + // mode, we don't want to vary the lightness so we just always forward the empty string instead. + return node.set('color', getNodeColor(node.get('rank'), '', node.get('pseudo'))); } export function nodeActiveMetricDecorator(node) { @@ -20,9 +22,10 @@ export function nodeActiveMetricDecorator(node) { const withCapacity = node.get('withCapacity'); const totalCapacity = withCapacity ? metric.get('max') : absoluteConsumption; const relativeConsumption = absoluteConsumption / totalCapacity; + const format = metric.get('format'); return node.set('activeMetric', makeMap({ - totalCapacity, absoluteConsumption, relativeConsumption, info + totalCapacity, absoluteConsumption, relativeConsumption, withCapacity, info, format })); } diff --git a/client/app/scripts/selectors/resource-view/layers.js b/client/app/scripts/selectors/resource-view/layers.js index dd5f127b9f..1b89d09afa 100644 --- a/client/app/scripts/selectors/resource-view/layers.js +++ b/client/app/scripts/selectors/resource-view/layers.js @@ -3,9 +3,9 @@ import { fromJS, Map as makeMap } from 'immutable'; import { createSelector } from 'reselect'; import { RESOURCES_LAYER_PADDING, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; -import { resourceViewLayers } from '../../constants/resources'; +import { resourceViewLayers, topologiesWithCapacity } from '../../constants/resources'; import { - nodeColorDecorator, + nodeResourceViewColorDecorator, nodeParentNodeDecorator, nodeResourceBoxDecorator, nodeActiveMetricDecorator, @@ -58,15 +58,16 @@ const decoratedNodesByTopologySelector = createSelector( topologiesNodes.forEach((topologyNodes, index) => { const layerTopologyId = layersTopologyIds.get(index); + const withCapacity = topologiesWithCapacity.includes(layerTopologyId); const decoratedTopologyNodes = (topologyNodes || makeMap()) .map(node => node.set('directParentTopologyId', lastLayerTopologyId)) .map(node => node.set('topologyId', layerTopologyId)) .map(node => node.set('activeMetricType', pinnedMetricType)) - .map(node => node.set('withCapacity', layerTopologyId === 'hosts')) + .map(node => node.set('withCapacity', withCapacity)) + .map(nodeResourceViewColorDecorator) .map(nodeActiveMetricDecorator) .map(nodeResourceBoxDecorator) - .map(nodeParentNodeDecorator) - .map(nodeColorDecorator); + .map(nodeParentNodeDecorator); const filteredTopologyNodes = decoratedTopologyNodes .filter(node => node.get('parentNodeId') || index === 0) .filter(node => node.get('width')); diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js index e67852a3f5..ef0afe771d 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,6 +54,24 @@ export function getMetricValue(metric) { }; } +// Used in the resource view +export function getHumanizedMetricInfo(metric) { + const showExtendedInfo = metric.withCapacity && metric.format !== 'percent'; + const totalCapacity = formatMetricSvg(metric.totalCapacity, metric); + const absoluteConsumption = formatMetricSvg(metric.absoluteConsumption, metric); + const relativeConsumption = formatMetricSvg(100 * metric.relativeConsumption, + { format: 'percent' }); + return ( + + + {showExtendedInfo ? relativeConsumption : absoluteConsumption} + consumed + {showExtendedInfo && {' - '} + ({absoluteConsumption} / {totalCapacity}) + } + + ); +} export function getMetricColor(metric) { const selectedMetric = metric && metric.get('id'); diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 8b0abf8c14..aeb7a7d2e6 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -52,7 +52,11 @@ &-selected { opacity: $btn-opacity-selected; } - &:hover { + &[disabled] { + cursor: default; + opacity: $btn-opacity-disabled; + } + &:not([disabled]):hover { opacity: $btn-opacity-hover; } } @@ -938,19 +942,24 @@ } .node-resource { - &-label .label-wrapper { + &-info { background-color: rgba(white, 0.6); border-radius: 2px; cursor: default; - display: block; - font-size: 15px; padding: 5px; + + .wrapper { + display: block; + + &.label { font-size: 15px; } + &.consumption { font-size: 13px; } + } } &-layer .layer-topology-name { background-color: rgba(#eee, 0.95); border: 1px solid #ccc; - color: #aaa; + color: $text-tertiary-color; font-size: 16px; font-weight: bold; padding-right: 20px; @@ -1231,7 +1240,7 @@ display: inline-block; background-color: $background-color; - &-selected, &:hover { + &-selected, &:not([disabled]):hover { color: $text-darker-color; background-color: $background-darker-color; } @@ -1271,7 +1280,7 @@ background-color: transparent; text-transform: uppercase; - &-selected, &:hover { + &-selected, &:not([disabled]):hover { background-color: $background-darker-secondary-color; } @@ -1638,13 +1647,13 @@ &-term { flex: 1; - color: #5b5b88; + color: $text-secondary-color; } &-term-label { flex: 1; b { - color: #5b5b88; + color: $text-secondary-color; } } } diff --git a/client/app/styles/_contrast-overrides.scss b/client/app/styles/_contrast-overrides.scss index 3cc85ed1b4..f957a26043 100644 --- a/client/app/styles/_contrast-overrides.scss +++ b/client/app/styles/_contrast-overrides.scss @@ -23,6 +23,7 @@ $edge-opacity-blurred: 0; $btn-opacity-default: 1; $btn-opacity-hover: 1; $btn-opacity-selected: 1; +$btn-opacity-disabled: 0.4; $link-opacity-default: 1; diff --git a/client/app/styles/_variables.scss b/client/app/styles/_variables.scss index 49bb144dcb..91ced8c557 100644 --- a/client/app/styles/_variables.scss +++ b/client/app/styles/_variables.scss @@ -44,6 +44,7 @@ $edge-color: rgb(110, 110, 156); $btn-opacity-default: 0.7; $btn-opacity-hover: 1; $btn-opacity-selected: 0.9; +$btn-opacity-disabled: 0.25; $link-opacity-default: 0.8; From f8348fc3a88f6b287afe157fd5bfeac4a68d66e1 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 17 Mar 2017 12:10:04 +0100 Subject: [PATCH 24/32] Switch to deep zooming transform in the resource view to avoid SVG precision errors --- .../app/scripts/charts/node-resource-box.js | 13 +++++---- .../app/scripts/charts/node-resource-info.js | 7 +++-- .../scripts/charts/nodes-resources-layer.js | 9 +++--- .../components/cachable-zoom-wrapper.js | 1 + client/app/scripts/decorators/node.js | 2 +- .../scripts/selectors/resource-view/layers.js | 29 ++++++++++--------- client/app/scripts/utils/metric-utils.js | 8 ++--- client/app/styles/_base.scss | 2 +- 8 files changed, 40 insertions(+), 31 deletions(-) diff --git a/client/app/scripts/charts/node-resource-box.js b/client/app/scripts/charts/node-resource-box.js index 5ba4a0a7e9..03b470a8f2 100644 --- a/client/app/scripts/charts/node-resource-box.js +++ b/client/app/scripts/charts/node-resource-box.js @@ -4,15 +4,16 @@ import { connect } from 'react-redux'; class NodeResourceBox extends React.Component { 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 translateY = this.props.height * (1 - relativeHeight); return { - transform: `translate(0, ${translateY})`, + transform: `translate(0, ${innerTranslateY})`, opacity: this.props.contrastMode ? 1 : 0.85, - height: this.props.height * relativeHeight, - width: this.props.width, - x: this.props.x, - y: this.props.y, + 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, diff --git a/client/app/scripts/charts/node-resource-info.js b/client/app/scripts/charts/node-resource-info.js index c58a14f94b..29cf6cf7b0 100644 --- a/client/app/scripts/charts/node-resource-info.js +++ b/client/app/scripts/charts/node-resource-info.js @@ -2,13 +2,16 @@ import React from 'react'; import { getHumanizedMetricInfo } from '../utils/metric-utils'; + +const HEIGHT = '45px'; + export default class NodeResourceInfo extends React.Component { render() { const { node, width, x, y } = this.props; - const humanizedMetricInfo = getHumanizedMetricInfo(node.get('activeMetric').toJS()); + const humanizedMetricInfo = getHumanizedMetricInfo(node.get('activeMetric')); return ( - + {node.get('label')} {humanizedMetricInfo} diff --git a/client/app/scripts/charts/nodes-resources-layer.js b/client/app/scripts/charts/nodes-resources-layer.js index 11618a3101..a6767cc9ae 100644 --- a/client/app/scripts/charts/nodes-resources-layer.js +++ b/client/app/scripts/charts/nodes-resources-layer.js @@ -11,9 +11,9 @@ import { } from '../selectors/resource-view/layers'; -const stringifiedTransform = ({ scaleX = 1, scaleY = 1, translateX = 0, translateY = 0 }) => ( - `translate(${translateX},${translateY}) scale(${scaleX},${scaleY})` -); +// const stringifiedTransform = ({ scaleX = 1, scaleY = 1, translateX = 0, translateY = 0 }) => ( +// `translate(${translateX},${translateY}) scale(${scaleX},${scaleY})` +// ); class NodesResourcesLayer extends React.Component { render() { @@ -21,7 +21,7 @@ class NodesResourcesLayer extends React.Component { return ( - + {nodes.toIndexedSeq().map(node => ( ))} diff --git a/client/app/scripts/components/cachable-zoom-wrapper.js b/client/app/scripts/components/cachable-zoom-wrapper.js index e0f6668d05..86aa674845 100644 --- a/client/app/scripts/components/cachable-zoom-wrapper.js +++ b/client/app/scripts/components/cachable-zoom-wrapper.js @@ -147,6 +147,7 @@ class CachableZoomWrapper extends React.Component { zoomed() { if (!this.props.disabled) { + console.log('Current zoom', d3Event.transform.x, d3Event.transform.k); const updatedState = this.cachableState({ scaleX: d3Event.transform.k, scaleY: d3Event.transform.k, diff --git a/client/app/scripts/decorators/node.js b/client/app/scripts/decorators/node.js index 49cf0a4fa7..8e70bf6eb3 100644 --- a/client/app/scripts/decorators/node.js +++ b/client/app/scripts/decorators/node.js @@ -31,7 +31,7 @@ export function nodeActiveMetricDecorator(node) { export function nodeResourceBoxDecorator(node) { const widthCriterion = node.get('withCapacity') ? 'totalCapacity' : 'absoluteConsumption'; - const width = node.getIn(['activeMetric', widthCriterion]) * 1e-5; + const width = node.getIn(['activeMetric', widthCriterion]) * 1e-4; const height = RESOURCES_LAYER_HEIGHT; return node.merge(makeMap({ width, height })); diff --git a/client/app/scripts/selectors/resource-view/layers.js b/client/app/scripts/selectors/resource-view/layers.js index 1b89d09afa..e1bc2e14d1 100644 --- a/client/app/scripts/selectors/resource-view/layers.js +++ b/client/app/scripts/selectors/resource-view/layers.js @@ -72,6 +72,8 @@ const decoratedNodesByTopologySelector = createSelector( .filter(node => node.get('parentNodeId') || index === 0) .filter(node => node.get('width')); + // console.log('Max width', filteredTopologyNodes.map(n => n.get('width')).max()); + // console.log('Min width', filteredTopologyNodes.map(n => n.get('width')).min()); nodesByTopology = nodesByTopology.set(layerTopologyId, filteredTopologyNodes); lastLayerTopologyId = layerTopologyId; }); @@ -102,19 +104,20 @@ export const positionedNodesByTopologySelector = createSelector( offset += node.get('width'); }); - // const offset = result.getIn([parentTopologyId, parentNodeId, 'x'], 0); - // const overhead = - // (x - offset) / result.getIn([parentTopologyId, parentNodeId, 'width'], x); - // if (overhead > 1) { - // console.log(overhead); - // bucket.forEach((_, nodeId) => { - // const node = result.getIn([layerTopologyId, nodeId]); - // result = result.mergeIn([layerTopologyId, nodeId], makeMap({ - // x: ((node.get('x') - offset) / overhead) + offset, - // width: node.get('width') / overhead, - // })); - // }); - // } + // TODO: Get rid of this disgusting code + const parentOffset = result.getIn([parentTopologyId, parentNodeId, 'offset'], 0); + const parentWidth = result.getIn([parentTopologyId, parentNodeId, 'width'], offset); + const overhead = (offset - parentOffset) / parentWidth; + if (overhead > 1) { + console.log(overhead); + bucket.forEach((_, nodeId) => { + const node = result.getIn([layerTopologyId, nodeId]); + result = result.mergeIn([layerTopologyId, nodeId], makeMap({ + x: ((node.get('offset') - parentOffset) / overhead) + parentOffset, + width: node.get('width') / overhead, + })); + }); + } }); }); diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js index ef0afe771d..3f201855d1 100644 --- a/client/app/scripts/utils/metric-utils.js +++ b/client/app/scripts/utils/metric-utils.js @@ -56,10 +56,10 @@ export function getMetricValue(metric) { // Used in the resource view export function getHumanizedMetricInfo(metric) { - const showExtendedInfo = metric.withCapacity && metric.format !== 'percent'; - const totalCapacity = formatMetricSvg(metric.totalCapacity, metric); - const absoluteConsumption = formatMetricSvg(metric.absoluteConsumption, metric); - const relativeConsumption = formatMetricSvg(100 * metric.relativeConsumption, + 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 ( diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index aeb7a7d2e6..81c658179f 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -952,7 +952,7 @@ display: block; &.label { font-size: 15px; } - &.consumption { font-size: 13px; } + &.consumption { font-size: 12px; } } } From 287d37923332f9b8003744f66183ed209c4d4dd8 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 17 Mar 2017 12:25:35 +0100 Subject: [PATCH 25/32] Renamed and moved resource view components --- .../scripts/components/cachable-zoom-wrapper.js | 1 - .../{charts => components}/nodes-resources.js | 6 +++--- .../node-resources-layer-labels-overlay.js} | 8 ++++---- .../node-resources-layer-topology.js} | 4 ++-- .../nodes-resources/node-resources-layer.js} | 14 +++++++------- .../node-resources-metric-box-info.js} | 4 ++-- .../nodes-resources/node-resources-metric-box.js} | 4 ++-- client/app/scripts/components/nodes.js | 2 +- client/app/scripts/decorators/node.js | 2 +- .../app/scripts/selectors/resource-view/layers.js | 2 -- 10 files changed, 22 insertions(+), 25 deletions(-) rename client/app/scripts/{charts => components}/nodes-resources.js (85%) rename client/app/scripts/{charts/layer-labels-overlay.js => components/nodes-resources/node-resources-layer-labels-overlay.js} (85%) rename client/app/scripts/{charts/layer-topology-name.js => components/nodes-resources/node-resources-layer-topology.js} (87%) rename client/app/scripts/{charts/nodes-resources-layer.js => components/nodes-resources/node-resources-layer.js} (79%) rename client/app/scripts/{charts/node-resource-info.js => components/nodes-resources/node-resources-metric-box-info.js} (78%) rename client/app/scripts/{charts/node-resource-box.js => components/nodes-resources/node-resources-metric-box.js} (91%) diff --git a/client/app/scripts/components/cachable-zoom-wrapper.js b/client/app/scripts/components/cachable-zoom-wrapper.js index 86aa674845..e0f6668d05 100644 --- a/client/app/scripts/components/cachable-zoom-wrapper.js +++ b/client/app/scripts/components/cachable-zoom-wrapper.js @@ -147,7 +147,6 @@ class CachableZoomWrapper extends React.Component { zoomed() { if (!this.props.disabled) { - console.log('Current zoom', d3Event.transform.x, d3Event.transform.k); const updatedState = this.cachableState({ scaleX: d3Event.transform.k, scaleY: d3Event.transform.k, diff --git a/client/app/scripts/charts/nodes-resources.js b/client/app/scripts/components/nodes-resources.js similarity index 85% rename from client/app/scripts/charts/nodes-resources.js rename to client/app/scripts/components/nodes-resources.js index 474dfc92fa..c0c11a4e36 100644 --- a/client/app/scripts/charts/nodes-resources.js +++ b/client/app/scripts/components/nodes-resources.js @@ -1,10 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; -import Logo from '../components/logo'; +import Logo from './logo'; import { layersTopologyIdsSelector } from '../selectors/resource-view/layers'; -import CachableZoomWrapper from '../components/cachable-zoom-wrapper'; -import NodesResourcesLayer from './nodes-resources-layer'; +import CachableZoomWrapper from './cachable-zoom-wrapper'; +import NodesResourcesLayer from './nodes-resources/node-resources-layer'; class NodesResources extends React.Component { diff --git a/client/app/scripts/charts/layer-labels-overlay.js b/client/app/scripts/components/nodes-resources/node-resources-layer-labels-overlay.js similarity index 85% rename from client/app/scripts/charts/layer-labels-overlay.js rename to client/app/scripts/components/nodes-resources/node-resources-layer-labels-overlay.js index 1163d570b8..3f9d0e1885 100644 --- a/client/app/scripts/charts/layer-labels-overlay.js +++ b/client/app/scripts/components/nodes-resources/node-resources-layer-labels-overlay.js @@ -1,13 +1,13 @@ import React from 'react'; import { fromJS } from 'immutable'; -import NodeResourceInfo from './node-resource-info'; -import { applyTransformX, applyTransformY } from '../utils/transform-utils'; +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'; +} from '../../constants/styles'; export default class LayerLabelsOverlay extends React.Component { @@ -39,7 +39,7 @@ export default class LayerLabelsOverlay extends React.Component { return ( {this.positionedLabels().map(label => ( - ( @@ -23,7 +23,7 @@ class NodesResourcesLayer extends React.Component { {nodes.toIndexedSeq().map(node => ( - ))} - - {!nodes.isEmpty() && node.get('parentNodeId') || index === 0) .filter(node => node.get('width')); - // console.log('Max width', filteredTopologyNodes.map(n => n.get('width')).max()); - // console.log('Min width', filteredTopologyNodes.map(n => n.get('width')).min()); nodesByTopology = nodesByTopology.set(layerTopologyId, filteredTopologyNodes); lastLayerTopologyId = layerTopologyId; }); From a52c979a7789c36a132d8a6341760edd79431bea Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 17 Mar 2017 16:56:41 +0100 Subject: [PATCH 26/32] Polished all the resource view components --- client/app/scripts/actions/app-actions.js | 42 +++---- client/app/scripts/charts/node.js | 1 - client/app/scripts/charts/nodes-chart.js | 59 +++++----- client/app/scripts/charts/nodes-grid.js | 1 + client/app/scripts/components/app.js | 1 - .../components/cachable-zoom-wrapper.js | 31 +++-- client/app/scripts/components/help-panel.js | 4 +- client/app/scripts/components/logo.js | 111 +++++++++--------- .../app/scripts/components/nodes-resources.js | 8 +- .../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 ++++++++++++--- .../components/troubleshooting-menu.js | 12 +- .../scripts/selectors/resource-view/layers.js | 5 +- client/app/scripts/utils/file-utils.js | 8 +- client/app/scripts/utils/metric-utils.js | 21 +--- client/app/scripts/utils/transform-utils.js | 17 ++- client/app/scripts/utils/web-api-utils.js | 8 +- client/app/styles/_base.scss | 35 +++--- 21 files changed, 321 insertions(+), 283 deletions(-) delete mode 100644 client/app/scripts/components/nodes-resources/node-resources-layer-labels-overlay.js diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 7ff8ab9b35..c96ad4e644 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -351,6 +351,25 @@ export function clickResumeUpdate() { }; } +function updateTopology(dispatch, getState) { + const state = getState(); + updateRoute(getState); + // update all request workers with new options + resetUpdateBuffer(); + // NOTE: This is currently not needed for our static resource + // view,but we'll need it here later and it's simpler to just + // keep it than to redo the nodes delta updating logic. + getNodesDelta( + getCurrentTopologyUrl(state), + activeTopologyOptionsSelector(state), + dispatch + ); + // Update the nodes for all topologies that appear in the current resource view. + if (isResourceViewModeSelector(state)) { + getResourceViewNodesSnapshot(getState, dispatch); + } +} + export function clickShowTopologyForNode(topologyId, nodeId) { return (dispatch, getState) => { dispatch({ @@ -358,15 +377,7 @@ export function clickShowTopologyForNode(topologyId, nodeId) { topologyId, nodeId }); - updateRoute(getState); - // update all request workers with new options - resetUpdateBuffer(); - const state = getState(); - getNodesDelta( - getCurrentTopologyUrl(state), - activeTopologyOptionsSelector(state), - dispatch - ); + updateTopology(dispatch, getState); }; } @@ -376,15 +387,7 @@ export function clickTopology(topologyId) { type: ActionTypes.CLICK_TOPOLOGY, topologyId }); - updateRoute(getState); - // update all request workers with new options - resetUpdateBuffer(); - const state = getState(); - getNodesDelta( - getCurrentTopologyUrl(state), - activeTopologyOptionsSelector(state), - dispatch - ); + updateTopology(dispatch, getState); }; } @@ -732,8 +735,7 @@ export function route(urlState) { // nodes for the current topology, but also the nodes of all the topologies that make // the layers in the resource view. if (isResourceViewModeSelector(state)) { - // Get all the nodes for the current resource view layout in the next run-cycle. - setTimeout(() => { getResourceViewNodesSnapshot(getState, dispatch); }, 0); + getResourceViewNodesSnapshot(getState, dispatch); } }; } diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index ab69437b94..84f3866def 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -97,7 +97,6 @@ class Node extends React.Component { exportingGraph, showingNetworks, stack, id, metric } = this.props; const { hovered } = this.state; - // console.log(metric && metric.toJS()); const color = getNodeColor(rank, label, pseudo); const truncate = !focused && !hovered; const labelOffsetY = (showingNetworks && networks) ? 40 : 28; diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index c825990b80..0dd9457754 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -7,6 +7,26 @@ import CachableZoomWrapper from '../components/cachable-zoom-wrapper'; import { clickBackground } from '../actions/app-actions'; +const EdgeMarkerDefinition = ({ selectedNodeId }) => { + const markerOffset = selectedNodeId ? '35' : '40'; + const markerSize = selectedNodeId ? '10' : '30'; + return ( + + + + + + ); +}; + class NodesChart extends React.Component { constructor(props, context) { super(props, context); @@ -14,45 +34,26 @@ class NodesChart extends React.Component { this.handleMouseClick = this.handleMouseClick.bind(this); } - render() { - // TODO: What to do with empty? - const { isEmpty, selectedNodeId } = this.props; - const markerOffset = selectedNodeId ? '35' : '40'; - const markerSize = selectedNodeId ? '10' : '30'; - const svgClassNames = isEmpty ? 'hide' : ''; + handleMouseClick() { + if (this.props.selectedNodeId) { + this.props.clickBackground(); + } + } + render() { + const { selectedNodeId } = this.props; return (
- - - - - - + - + +
); } - - handleMouseClick() { - if (this.props.selectedNodeId) { - this.props.clickBackground(); - } - } } diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js index 47f3d46e5b..719c3df2dc 100644 --- a/client/app/scripts/charts/nodes-grid.js +++ b/client/app/scripts/charts/nodes-grid.js @@ -105,6 +105,7 @@ class NodesGrid extends React.Component { paddingLeft: canvasMargins.left, paddingRight: canvasMargins.right, }; + // TODO: What are 24 and 18? Use a comment or extract into constants. const tbodyHeight = height - 24 - 18; const className = 'scroll-body'; const tbodyStyle = { diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 1ea2871ae8..03c82fa2f1 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -112,7 +112,6 @@ class App extends React.Component { showingMetricsSelector, showingNetworkSelector, showingTroubleshootingMenu } = this.props; const isIframe = window !== window.top; - // TODO: Remove 'grid', 'topo' constants. return (
{showingDebugToolbar() && } diff --git a/client/app/scripts/components/cachable-zoom-wrapper.js b/client/app/scripts/components/cachable-zoom-wrapper.js index e0f6668d05..0804a8edc9 100644 --- a/client/app/scripts/components/cachable-zoom-wrapper.js +++ b/client/app/scripts/components/cachable-zoom-wrapper.js @@ -7,6 +7,7 @@ import { event as d3Event, select } from 'd3-selection'; import { zoom, zoomIdentity } from 'd3-zoom'; import { cacheZoomState } from '../actions/app-actions'; +import { transformToString } from '../utils/transform-utils'; import { activeLayoutZoomSelector } from '../selectors/zooming'; import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/topology'; import { @@ -42,8 +43,7 @@ class CachableZoomWrapper extends React.Component { componentDidMount() { this.zoomRestored = false; this.zoom = zoom().on('zoom', this.zoomed); - // TODO: Make this correct - this.svg = select('.nodes-chart svg'); + this.svg = select(`svg#${this.props.svg}`); this.setZoomTriggers(!this.props.disabled); this.restoreCachedZoom(this.props); @@ -77,13 +77,15 @@ class CachableZoomWrapper extends React.Component { } render() { + // `forwardTransform` says whether the zoom transform is forwarded to the child + // component. The advantage of that is more control rendering control in the + // children, while the disadvantage is that it's slower, as all the children + // get updated on every zoom/pan action. const { children, forwardTransform } = this.props; - const { translateX, translateY, scaleX, scaleY } = this.state; - const transform = `translate(${translateX},${translateY}) scale(${scaleX},${scaleY})`; + const transform = forwardTransform ? '' : transformToString(this.state); - // Not passing transform into child components by default for perf reasons. return ( - + {forwardTransform ? children(this.state) : children} ); @@ -97,7 +99,12 @@ class CachableZoomWrapper extends React.Component { } } + // Decides which part of the zoom state is cachable depending + // on the horizontal/vertical degrees of freedom. cachableState(state = this.state) { + // TODO: Probably shouldn't cache the limits if the layout can + // change a lot. However, before removing them from here, we have + // to make sure we can always get them from the default zooms. let cachableFields = [ 'minTranslateX', 'maxTranslateX', 'minTranslateY', 'maxTranslateY', @@ -116,30 +123,38 @@ class CachableZoomWrapper extends React.Component { this.props.cacheZoomState(fromJS(this.cachableState())); } + // Restore the zooming settings restoreCachedZoom(props) { if (!props.layoutZoom.isEmpty()) { const zoomState = props.layoutZoom.toJS(); - // Restore the zooming settings + // Scaling limits are always set. this.zoom = this.zoom.scaleExtent([zoomState.minScale, zoomState.maxScale]); + // Translation limits are optional. if (props.bounded) { this.zoom = this.zoom + // Translation limits are only set if explicitly demanded (currently we are using them + // in the resource view, but not in the graph view, although I think the idea would be + // to use them everywhere). .translateExtent([ [zoomState.minTranslateX, zoomState.minTranslateY], [zoomState.maxTranslateX, zoomState.maxTranslateY], ]) + // This is to ensure that the translation limits are properly + // centered, so that the canvas margins are respected. .extent([ [props.canvasMargins.left, props.canvasMargins.top], [props.canvasMargins.left + props.width, props.canvasMargins.top + props.height] ]); } + // After the limits have been set, update the zoom. this.svg.call(this.zoom.transform, zoomIdentity .translate(zoomState.translateX, zoomState.translateY) .scale(zoomState.scaleX, zoomState.scaleY)); - // Update the state variables + // Update the state variables. this.setState(zoomState); this.zoomRestored = true; } diff --git a/client/app/scripts/components/help-panel.js b/client/app/scripts/components/help-panel.js index e20e9132ff..2ee6fda96d 100644 --- a/client/app/scripts/components/help-panel.js +++ b/client/app/scripts/components/help-panel.js @@ -149,10 +149,10 @@ function renderFieldsPanel(currentTopologyName, searchableFields) { } -function HelpPanel({currentTopologyName, searchableFields, onClickClose}) { +function HelpPanel({ currentTopologyName, searchableFields, onClickClose, canvasMargins }) { return (
-
+

Help

diff --git a/client/app/scripts/components/logo.js b/client/app/scripts/components/logo.js index 9d9a54a8f1..71160f210f 100644 --- a/client/app/scripts/components/logo.js +++ b/client/app/scripts/components/logo.js @@ -2,61 +2,58 @@ /* eslint max-len: "off" */ import React from 'react'; -export default class Logo extends React.Component { - render() { - const { transform = '' } = this.props; - return ( - - - - - - - - - - - - - - - - - - - ); - } +export default function Logo({ transform = '' }) { + return ( + + + + + + + + + + + + + + + + + + + ); } diff --git a/client/app/scripts/components/nodes-resources.js b/client/app/scripts/components/nodes-resources.js index c0c11a4e36..aeecaf548c 100644 --- a/client/app/scripts/components/nodes-resources.js +++ b/client/app/scripts/components/nodes-resources.js @@ -21,12 +21,10 @@ class NodesResources extends React.Component { render() { return ( -
- +
+ - + {transform => this.renderLayers(transform)} 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..519b0b2e84 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 transform 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; // px + + // 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/components/troubleshooting-menu.js b/client/app/scripts/components/troubleshooting-menu.js index c58753ca24..9da47f9cab 100644 --- a/client/app/scripts/components/troubleshooting-menu.js +++ b/client/app/scripts/components/troubleshooting-menu.js @@ -38,7 +38,7 @@ class DebugMenu extends React.Component {
- {!this.props.gridMode &&
+
-
} +
node.get('parentNodeId') || index === 0) + .filter(node => nodesByTopology.hasIn([lastLayerTopologyId, node.get('parentNodeId')]) + || index === 0) .filter(node => node.get('width')); nodesByTopology = nodesByTopology.set(layerTopologyId, filteredTopologyNodes); @@ -107,7 +108,7 @@ export const positionedNodesByTopologySelector = createSelector( const parentWidth = result.getIn([parentTopologyId, parentNodeId, 'width'], offset); const overhead = (offset - parentOffset) / parentWidth; if (overhead > 1) { - console.log(overhead); + // console.log(overhead); bucket.forEach((_, nodeId) => { const node = result.getIn([layerTopologyId, nodeId]); result = result.mergeIn([layerTopologyId, nodeId], makeMap({ diff --git a/client/app/scripts/utils/file-utils.js b/client/app/scripts/utils/file-utils.js index 002b14f2d9..d67cfcfc1c 100644 --- a/client/app/scripts/utils/file-utils.js +++ b/client/app/scripts/utils/file-utils.js @@ -93,8 +93,12 @@ function download(source, name) { }, 10); } +function getSVGElement() { + return document.getElementById('canvas'); +} + function getSVG(doc, emptySvgDeclarationComputed) { - const svg = document.getElementById('nodes-chart-canvas'); + const svg = getSVGElement(); const target = svg.cloneNode(true); target.setAttribute('version', '1.1'); @@ -127,7 +131,7 @@ function cleanup() { }); // hide embedded logo - const svg = document.getElementById('nodes-chart-canvas'); + const svg = getSVGElement(); svg.setAttribute('class', ''); } 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..21160ae79b 100644 --- a/client/app/scripts/utils/transform-utils.js +++ b/client/app/scripts/utils/transform-utils.js @@ -1,3 +1,16 @@ -export const applyTransformX = ({ scaleX = 1, translateX = 0 }, x) => (x * scaleX) + translateX; -export const applyTransformY = ({ scaleY = 1, translateY = 0 }, y) => (y * scaleY) + translateY; +const applyTranslateX = ({ scaleX = 1, translateX = 0 }, x) => (x * scaleX) + translateX; +const applyTranslateY = ({ scaleY = 1, translateY = 0 }, y) => (y * scaleY) + translateY; +const applyScaleX = ({ scaleX = 1 }, width) => width * scaleX; +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), +}); + +export const transformToString = ({ translateX = 0, translateY = 0, scaleX = 1, scaleY = 1 }) => ( + `translate(${translateX},${translateY}) scale(${scaleX},${scaleY})` +); diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index dd118cc625..34ce7dbf30 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -188,7 +188,11 @@ export function getAllNodes(getState, dispatch) { // will probably want to change this. export function getResourceViewNodesSnapshot(getState, dispatch) { const topologyIds = layersTopologyIdsSelector(getState()); - getNodesForTopologies(getState, dispatch, topologyIds); + // TODO: Try to get rid of the timeout (I noticed it was necessary in + // some circumstances, but would have to investigate when exactly). + setTimeout(() => { + getNodesForTopologies(getState, dispatch, topologyIds); + }, 100); } export function getTopologies(options, dispatch, initialPoll) { @@ -220,6 +224,8 @@ export function getTopologies(options, dispatch, initialPoll) { }); } +// TODO: topologyUrl and options are always used for the current topology so they as arguments +// can be replaced by the `state` and then retrieved here internally from selectors. export function getNodesDelta(topologyUrl, options, dispatch) { const optionsQuery = buildOptionsQuery(options); // Only recreate websocket if url changed or if forced (weave cloud instance reload); diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 81c658179f..369d28ac7b 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -249,7 +249,7 @@ } -.nodes-chart { +.nodes-chart, .nodes-resources { &-error, &-loading { @extend .hideable; @@ -307,11 +307,6 @@ transition: opacity .2s $base-ease; text-align: center; - .node-network { - // stroke: $background-lighter-color; - // stroke-width: 4px; - } - .node-label { color: $text-color; } @@ -941,22 +936,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; - &.label { font-size: 15px; } - &.consumption { font-size: 12px; } + .wrapper { + display: block; + + &.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; @@ -1685,7 +1684,7 @@ // Notes: Firefox gets a bit messy if you try and bubble // heights + overflow up (min-height issue + still doesn't work v.well), // so this is a bit of a hack. - max-height: calc(100vh - 160px - 160px - 160px); + max-height: "calc(100vh - 160px - 160px - 160px)"; } } } From a8d8e0e2b8317f023237f17456d949d57220818c Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 20 Mar 2017 16:57:27 +0100 Subject: [PATCH 27/32] Changing the available metrics selection --- client/app/scripts/actions/app-actions.js | 53 ++++++++++++++------- client/app/scripts/reducers/root.js | 12 +++-- client/app/scripts/selectors/node-metric.js | 10 ++++ client/app/scripts/utils/web-api-utils.js | 8 +--- client/app/styles/_base.scss | 3 +- 5 files changed, 56 insertions(+), 30 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index c96ad4e644..79e99011cf 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -24,6 +24,7 @@ import { import { getCurrentTopologyUrl } from '../utils/topology-utils'; import { storageSet } from '../utils/storage-utils'; import { loadTheme } from '../utils/contrast-utils'; +import { pinnedMetricSelector } from '../selectors/node-metric'; import { activeTopologyOptionsSelector, isResourceViewModeSelector, @@ -257,12 +258,41 @@ export function clickForceRelayout() { }; } +export function doSearch(searchQuery) { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.DO_SEARCH, + searchQuery + }); + updateRoute(getState); + }; +} + export function setViewportDimensions(width, height) { return (dispatch) => { dispatch({ type: ActionTypes.SET_VIEWPORT_DIMENSIONS, width, height }); }; } +function updateResourceViewState(getState, dispatch) { + // Clear the query as searching is not yet supported in the resource view. + dispatch({ type: ActionTypes.DO_SEARCH, searchQuery: '' }); + + // Resource view requires a pinned metric, so pin the first one if none was pinned. + console.log(pinnedMetricSelector(getState()), getState().get('availableCanvasMetrics')); + if (!pinnedMetricSelector(getState())) { + dispatch({ type: ActionTypes.PIN_METRIC }); + } + + // Update the nodes for all topologies that appear in the current resource view. The timeout + // seems to be necessary in some situations, but could possibly be skipped in some scenarios. + // TODO: Find a more elegant way of fetching the topologies information, + // and also make it periodically update. + setTimeout(() => { + getResourceViewNodesSnapshot(getState, dispatch); + }, 100); +} + export function setGraphView() { return (dispatch, getState) => { dispatch({ @@ -289,9 +319,8 @@ export function setResourceView() { type: ActionTypes.SET_VIEW_MODE, viewMode: RESOURCE_VIEW_MODE, }); + updateResourceViewState(getState, dispatch); updateRoute(getState); - // Update the nodes for all topologies that appear in the current resource view. - getResourceViewNodesSnapshot(getState, dispatch); }; } @@ -353,6 +382,10 @@ export function clickResumeUpdate() { function updateTopology(dispatch, getState) { const state = getState(); + // Update the resource view state. + if (isResourceViewModeSelector(state)) { + updateResourceViewState(getState, dispatch); + } updateRoute(getState); // update all request workers with new options resetUpdateBuffer(); @@ -364,10 +397,6 @@ function updateTopology(dispatch, getState) { activeTopologyOptionsSelector(state), dispatch ); - // Update the nodes for all topologies that appear in the current resource view. - if (isResourceViewModeSelector(state)) { - getResourceViewNodesSnapshot(getState, dispatch); - } } export function clickShowTopologyForNode(topologyId, nodeId) { @@ -428,16 +457,6 @@ export function doControl(nodeId, control) { }; } -export function doSearch(searchQuery) { - return (dispatch, getState) => { - dispatch({ - type: ActionTypes.DO_SEARCH, - searchQuery - }); - updateRoute(getState); - }; -} - export function enterEdge(edgeId) { return { type: ActionTypes.ENTER_EDGE, @@ -735,7 +754,7 @@ export function route(urlState) { // nodes for the current topology, but also the nodes of all the topologies that make // the layers in the resource view. if (isResourceViewModeSelector(state)) { - getResourceViewNodesSnapshot(getState, dispatch); + updateResourceViewState(getState, dispatch); } }; } diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 818745604b..eb80f03150 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -375,12 +375,14 @@ export function rootReducer(state = initialState, action) { } case ActionTypes.PIN_METRIC: { - const metricTypes = makeMap( - state.get('availableCanvasMetrics').map(m => [m.get('id'), m.get('label')])); + const canvasMetrics = state.get('availableCanvasMetrics'); + const metricTypes = makeMap(canvasMetrics.map(m => [m.get('id'), m.get('label')])); + // Pin the first metric if no metric ID was explicitly given. + const metricId = action.metricId || (canvasMetrics.first() || makeMap()).get('id'); return state.merge({ - pinnedMetric: action.metricId, - pinnedMetricType: metricTypes.get(action.metricId), - selectedMetric: action.metricId + pinnedMetric: metricId, + pinnedMetricType: metricTypes.get(metricId), + selectedMetric: metricId, }); } diff --git a/client/app/scripts/selectors/node-metric.js b/client/app/scripts/selectors/node-metric.js index eb1445abab..3547991ebe 100644 --- a/client/app/scripts/selectors/node-metric.js +++ b/client/app/scripts/selectors/node-metric.js @@ -3,6 +3,16 @@ import { createMapSelector } from 'reselect-map'; import { fromJS } from 'immutable'; +export const pinnedMetricSelector = createSelector( + [ + state => state.get('availableCanvasMetrics'), + state => state.get('pinnedMetricType'), + ], + (availableMetrics, pinnedMetricType) => ( + availableMetrics.find(metric => metric.get('label') === pinnedMetricType) + ) +); + const topCardNodeSelector = createSelector( [ state => state.get('nodeDetails') diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 34ce7dbf30..77dd7035e9 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -178,8 +178,8 @@ function getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions */ export function getAllNodes(getState, dispatch) { const state = getState(); - const topologyIds = state.get('topologies').map(topology => topology.get('id')); const topologyOptions = state.get('topologyOptions'); + const topologyIds = state.get('topologyUrlsById').keySeq(); getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions); } @@ -188,11 +188,7 @@ export function getAllNodes(getState, dispatch) { // will probably want to change this. export function getResourceViewNodesSnapshot(getState, dispatch) { const topologyIds = layersTopologyIdsSelector(getState()); - // TODO: Try to get rid of the timeout (I noticed it was necessary in - // some circumstances, but would have to investigate when exactly). - setTimeout(() => { - getNodesForTopologies(getState, dispatch, topologyIds); - }, 100); + getNodesForTopologies(getState, dispatch, topologyIds); } export function getTopologies(options, dispatch, initialPoll) { diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 369d28ac7b..5415f72355 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -1229,7 +1229,6 @@ border-radius: $border-radius; border: 1px solid $background-darker-color; display: inline-block; - margin-left: 20px; } &-action { @@ -1259,7 +1258,7 @@ .view-mode-selector { margin-top: 8px; - margin-left: 8px; + margin-left: 20px; min-width: 161px; &-wrapper { From 471251a5d5215515210d5ad12a4f00913efd70a4 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 20 Mar 2017 19:30:02 +0100 Subject: [PATCH 28/32] Improved and polished the state transition logic for the resource view --- client/app/scripts/actions/app-actions.js | 38 ++++++----------- client/app/scripts/components/app.js | 3 +- .../app/scripts/components/debug-toolbar.js | 7 ++-- .../components/metric-selector-item.js | 5 ++- .../app/scripts/components/metric-selector.js | 15 +++++-- client/app/scripts/components/search.js | 7 ++-- client/app/scripts/components/topologies.js | 8 +++- client/app/scripts/reducers/root.js | 41 +++++-------------- client/app/scripts/selectors/node-metric.js | 36 +++++++++++++--- .../scripts/selectors/resource-view/layers.js | 2 +- client/app/scripts/selectors/topology.js | 17 +++++++- client/app/scripts/utils/topology-utils.js | 8 +++- client/app/scripts/utils/web-api-utils.js | 6 ++- client/app/styles/_base.scss | 10 ++++- 14 files changed, 121 insertions(+), 82 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 79e99011cf..22b2fc2aed 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -24,7 +24,7 @@ import { import { getCurrentTopologyUrl } from '../utils/topology-utils'; import { storageSet } from '../utils/storage-utils'; import { loadTheme } from '../utils/contrast-utils'; -import { pinnedMetricSelector } from '../selectors/node-metric'; +import { availableMetricsSelector, pinnedMetricSelector } from '../selectors/node-metric'; import { activeTopologyOptionsSelector, isResourceViewModeSelector, @@ -151,7 +151,7 @@ export function unpinMetric() { export function pinNextMetric(delta) { return (dispatch, getState) => { const state = getState(); - const metrics = state.get('availableCanvasMetrics').map(m => m.get('id')); + const metrics = availableMetricsSelector(state).map(m => m.get('id')); const currentIndex = metrics.indexOf(state.get('selectedMetric')); const nextIndex = modulo(currentIndex + delta, metrics.count()); const nextMetric = metrics.get(nextIndex); @@ -274,25 +274,6 @@ export function setViewportDimensions(width, height) { }; } -function updateResourceViewState(getState, dispatch) { - // Clear the query as searching is not yet supported in the resource view. - dispatch({ type: ActionTypes.DO_SEARCH, searchQuery: '' }); - - // Resource view requires a pinned metric, so pin the first one if none was pinned. - console.log(pinnedMetricSelector(getState()), getState().get('availableCanvasMetrics')); - if (!pinnedMetricSelector(getState())) { - dispatch({ type: ActionTypes.PIN_METRIC }); - } - - // Update the nodes for all topologies that appear in the current resource view. The timeout - // seems to be necessary in some situations, but could possibly be skipped in some scenarios. - // TODO: Find a more elegant way of fetching the topologies information, - // and also make it periodically update. - setTimeout(() => { - getResourceViewNodesSnapshot(getState, dispatch); - }, 100); -} - export function setGraphView() { return (dispatch, getState) => { dispatch({ @@ -319,7 +300,11 @@ export function setResourceView() { type: ActionTypes.SET_VIEW_MODE, viewMode: RESOURCE_VIEW_MODE, }); - updateResourceViewState(getState, dispatch); + // Pin the first metric if none of the visible ones is pinned. + if (!pinnedMetricSelector(getState())) { + dispatch({ type: ActionTypes.PIN_METRIC }); + } + getResourceViewNodesSnapshot(getState, dispatch); updateRoute(getState); }; } @@ -382,15 +367,16 @@ export function clickResumeUpdate() { function updateTopology(dispatch, getState) { const state = getState(); - // Update the resource view state. + // If we're in the resource view, get the snapshot of all the relevant node topologies. + // TODO: Consider updating the state to always have a pinned metric. if (isResourceViewModeSelector(state)) { - updateResourceViewState(getState, dispatch); + getResourceViewNodesSnapshot(getState, dispatch); } updateRoute(getState); // update all request workers with new options resetUpdateBuffer(); // NOTE: This is currently not needed for our static resource - // view,but we'll need it here later and it's simpler to just + // view, but we'll need it here later and it's simpler to just // keep it than to redo the nodes delta updating logic. getNodesDelta( getCurrentTopologyUrl(state), @@ -754,7 +740,7 @@ export function route(urlState) { // nodes for the current topology, but also the nodes of all the topologies that make // the layers in the resource view. if (isResourceViewModeSelector(state)) { - updateResourceViewState(getState, dispatch); + getResourceViewNodesSnapshot(getState, dispatch); } }; } diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 03c82fa2f1..a1fd46f0e4 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -21,6 +21,7 @@ import MetricSelector from './metric-selector'; import NetworkSelector from './networks-selector'; import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar'; import { getRouter, getUrlState } from '../utils/router-utils'; +import { availableMetricsSelector } from '../selectors/node-metric'; import { availableNetworksSelector } from '../selectors/node-networks'; import { activeTopologyOptionsSelector, @@ -161,7 +162,7 @@ function mapStateToProps(state) { showingDetails: state.get('nodeDetails').size > 0, showingHelp: state.get('showingHelp'), showingTroubleshootingMenu: state.get('showingTroubleshootingMenu'), - showingMetricsSelector: state.get('availableCanvasMetrics').count() > 0, + showingMetricsSelector: availableMetricsSelector(state).count() > 0, showingNetworkSelector: availableNetworksSelector(state).count() > 0, showingTerminal: state.get('controlPipes').size > 0, urlState: getUrlState(state) diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 5ccc230273..3dd8c99f04 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -10,6 +10,7 @@ import debug from 'debug'; import ActionTypes from '../constants/action-types'; import { receiveNodesDelta } from '../actions/app-actions'; import { getNodeColor, getNodeColorDark, text2degree } from '../utils/color-utils'; +import { availableMetricsSelector } from '../selectors/node-metric'; const SHAPES = ['square', 'hexagon', 'heptagon', 'circle']; @@ -291,7 +292,7 @@ class DebugToolbar extends React.Component { } render() { - const { availableCanvasMetrics } = this.props; + const { availableMetrics } = this.props; return (
@@ -302,7 +303,7 @@ class DebugToolbar extends React.Component { - @@ -379,7 +380,7 @@ class DebugToolbar extends React.Component { function mapStateToProps(state) { return { nodes: state.get('nodes'), - availableCanvasMetrics: state.get('availableCanvasMetrics') + availableMetrics: availableMetricsSelector(state), }; } diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js index c1d2d5aab0..0abf0d065f 100644 --- a/client/app/scripts/components/metric-selector-item.js +++ b/client/app/scripts/components/metric-selector-item.js @@ -3,6 +3,7 @@ import classNames from 'classnames'; import { connect } from 'react-redux'; import { selectMetric, pinMetric, unpinMetric } from '../actions/app-actions'; +import { pinnedMetricSelector } from '../selectors/node-metric'; class MetricSelectorItem extends React.Component { @@ -30,7 +31,7 @@ class MetricSelectorItem extends React.Component { } render() { - const {metric, selectedMetric, pinnedMetric} = this.props; + const { metric, selectedMetric, pinnedMetric } = this.props; const id = metric.get('id'); const isPinned = (id === pinnedMetric); const isSelected = (id === selectedMetric); @@ -54,7 +55,7 @@ class MetricSelectorItem extends React.Component { function mapStateToProps(state) { return { selectedMetric: state.get('selectedMetric'), - pinnedMetric: state.get('pinnedMetric') + pinnedMetric: pinnedMetricSelector(state), }; } diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index 34435249e7..c77657286f 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { selectMetric } from '../actions/app-actions'; +import { availableMetricsSelector, pinnedMetricSelector } from '../selectors/node-metric'; import MetricSelectorItem from './metric-selector-item'; class MetricSelector extends React.Component { @@ -16,17 +17,23 @@ class MetricSelector extends React.Component { } render() { + const { pinnedMetric, alwaysPinned, availableMetrics } = this.props; + const shouldPinMetric = alwaysPinned && !pinnedMetric && !availableMetrics.isEmpty(); + return (
- {this.props.availableCanvasMetrics.map(metric => ( + {availableMetrics.map(metric => ( ))}
+ {shouldPinMetric && + « Select a metric + }
); } @@ -34,8 +41,8 @@ class MetricSelector extends React.Component { function mapStateToProps(state) { return { - availableCanvasMetrics: state.get('availableCanvasMetrics'), - pinnedMetric: state.get('pinnedMetric') + availableMetrics: availableMetricsSelector(state), + pinnedMetric: pinnedMetricSelector(state), }; } diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index 7572b67608..0b40a438f3 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -90,7 +90,7 @@ class Search extends React.Component { componentWillReceiveProps(nextProps) { // when cleared from the outside, reset internal state if (this.props.searchQuery !== nextProps.searchQuery && nextProps.searchQuery === '') { - this.setState({value: ''}); + this.setState({ value: '' }); } } @@ -105,14 +105,15 @@ class Search extends React.Component { render() { const { nodes, pinnedSearches, searchFocused, searchMatchCountByTopology, isResourceViewMode, searchQuery, topologiesLoaded, onClickHelp, inputId = 'search' } = this.props; - const disabled = this.props.isTopologyEmpty; + const hidden = !topologiesLoaded || isResourceViewMode; + const disabled = this.props.isTopologyEmpty && !hidden; const matchCount = searchMatchCountByTopology .reduce((count, topologyMatchCount) => count + topologyMatchCount, 0); const showPinnedSearches = pinnedSearches.size > 0; // manual clear (null) has priority, then props, then state const value = this.state.value === null ? '' : this.state.value || searchQuery || ''; const classNames = classnames('search', 'hideable', { - hide: !topologiesLoaded || isResourceViewMode, + hide: hidden, 'search-pinned': showPinnedSearches, 'search-matched': matchCount, 'search-filled': value, diff --git a/client/app/scripts/components/topologies.js b/client/app/scripts/components/topologies.js index 02bb501158..a5060ac990 100644 --- a/client/app/scripts/components/topologies.js +++ b/client/app/scripts/components/topologies.js @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import classnames from 'classnames'; import { searchMatchCountByTopologySelector } from '../selectors/search'; +import { isResourceViewModeSelector } from '../selectors/topology'; import { clickTopology } from '../actions/app-actions'; @@ -35,8 +36,9 @@ class Topologies extends React.Component { const searchMatchCount = this.props.searchMatchCountByTopology.get(topologyId) || 0; const title = basicTopologyInfo(subTopology, searchMatchCount); const className = classnames('topologies-sub-item', { + // Don't show matches in the resource view as searching is not supported there yet. + 'topologies-sub-item-matched': !this.props.isResourceViewMode && searchMatchCount, 'topologies-sub-item-active': isActive, - 'topologies-sub-item-matched': searchMatchCount }); return ( @@ -54,8 +56,9 @@ class Topologies extends React.Component { const isActive = topology === this.props.currentTopology; const searchMatchCount = this.props.searchMatchCountByTopology.get(topology.get('id')) || 0; const className = classnames('topologies-item-main', { + // Don't show matches in the resource view as searching is not supported there yet. + 'topologies-item-main-matched': !this.props.isResourceViewMode && searchMatchCount, 'topologies-item-main-active': isActive, - 'topologies-item-main-matched': searchMatchCount }); const topologyId = topology.get('id'); const title = basicTopologyInfo(topology, searchMatchCount); @@ -91,6 +94,7 @@ function mapStateToProps(state) { topologies: state.get('topologies'), currentTopology: state.get('currentTopology'), searchMatchCountByTopology: searchMatchCountByTopologySelector(state), + isResourceViewMode: isResourceViewModeSelector(state), }; } diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index eb80f03150..bc837d5fad 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -9,12 +9,13 @@ import { EDGE_ID_SEPARATOR, GRAPH_VIEW_MODE, TABLE_VIEW_MODE, - RESOURCE_VIEW_MODE, } from '../constants/naming'; import { graphExceedsComplexityThreshSelector, activeTopologyZoomCacheKeyPathSelector, + isResourceViewModeSelector, } from '../selectors/topology'; +import { availableMetricsSelector, pinnedMetricSelector } from '../selectors/node-metric'; import { applyPinnedSearches } from '../utils/search-utils'; import { findTopologyById, @@ -36,8 +37,6 @@ const topologySorter = topology => topology.get('rank'); // Initial values export const initialState = makeMap({ - availableCanvasMetrics: makeList(), - availableNetworks: makeList(), contrastMode: false, controlPipes: makeOrderedMap(), // pipeId -> controlPipe controlStatus: makeMap(), @@ -60,7 +59,6 @@ export const initialState = makeMap({ nodesLoaded: false, // nodes cache, infrequently updated, used for search & resource view nodesByTopology: makeMap(), // topologyId -> nodes - pinnedMetric: null, // class of metric, e.g. 'cpu', rather than 'host_cpu' or 'process_cpu'. // allows us to keep the same metric "type" selected when the topology changes. pinnedMetricType: null, @@ -312,7 +310,6 @@ export function rootReducer(state = initialState, action) { state = setTopology(state, action.topologyId); state = clearNodes(state); } - state = state.set('availableCanvasMetrics', makeList()); return state; } @@ -326,7 +323,6 @@ export function rootReducer(state = initialState, action) { state = clearNodes(state); } - state = state.set('availableCanvasMetrics', makeList()); return state; } @@ -375,22 +371,18 @@ export function rootReducer(state = initialState, action) { } case ActionTypes.PIN_METRIC: { - const canvasMetrics = state.get('availableCanvasMetrics'); + const canvasMetrics = availableMetricsSelector(state); const metricTypes = makeMap(canvasMetrics.map(m => [m.get('id'), m.get('label')])); // Pin the first metric if no metric ID was explicitly given. const metricId = action.metricId || (canvasMetrics.first() || makeMap()).get('id'); return state.merge({ - pinnedMetric: metricId, pinnedMetricType: metricTypes.get(metricId), selectedMetric: metricId, }); } case ActionTypes.UNPIN_METRIC: { - return state.merge({ - pinnedMetric: null, - pinnedMetricType: null - }); + return state.set('pinnedMetricType', null); } case ActionTypes.SHOW_HELP: { @@ -607,30 +599,19 @@ export function rootReducer(state = initialState, action) { // apply pinned searches, filters nodes that dont match state = applyPinnedSearches(state); - state = state.set('availableCanvasMetrics', state.get('nodes') - .valueSeq() - .flatMap(n => (n.get('metrics') || makeList()).map(m => ( - makeMap({id: m.get('id'), label: m.get('label')}) - ))) - .toSet() - .toList() - .sortBy(m => m.get('label'))); - - const similarTypeMetric = state.get('availableCanvasMetrics') - .find(m => m.get('label') === state.get('pinnedMetricType')); - state = state.set('pinnedMetric', similarTypeMetric && similarTypeMetric.get('id')); // if something in the current topo is not already selected, select it. - if (!state.get('availableCanvasMetrics') + if (!availableMetricsSelector(state) .map(m => m.get('id')) .toSet() .has(state.get('selectedMetric'))) { - state = state.set('selectedMetric', state.get('pinnedMetric')); + state = state.set('selectedMetric', pinnedMetricSelector(state)); } - // update nodes cache - if (state.get('topologyViewMode') !== RESOURCE_VIEW_MODE) { - state = state.setIn( - ['nodesByTopology', state.get('currentTopologyId')], state.get('nodes')); + // Update the nodes cache only if we're not in the resource view mode, as we + // intentionally want to keep it static before we figure how to keep it up-to-date. + if (!isResourceViewModeSelector(state)) { + const nodesForCurrentTopologyKey = ['nodesByTopology', state.get('currentTopologyId')]; + state = state.setIn(nodesForCurrentTopologyKey, state.get('nodes')); } return state; diff --git a/client/app/scripts/selectors/node-metric.js b/client/app/scripts/selectors/node-metric.js index 3547991ebe..7f54ce7a57 100644 --- a/client/app/scripts/selectors/node-metric.js +++ b/client/app/scripts/selectors/node-metric.js @@ -1,16 +1,42 @@ import { createSelector } from 'reselect'; import { createMapSelector } from 'reselect-map'; -import { fromJS } from 'immutable'; +import { fromJS, Map as makeMap, List as makeList } from 'immutable'; +import { + isResourceViewModeSelector, + cachedCurrentTopologyNodesSelector, +} from '../selectors/topology'; + + +// Resource view uses the metrics of the nodes from the cache, while the graph and table +// view are looking at the current nodes (which are among other things filtered by topology +// options which are currently ignored in the resource view). +export const availableMetricsSelector = createSelector( + [ + isResourceViewModeSelector, + cachedCurrentTopologyNodesSelector, + state => state.get('nodes'), + ], + (isResourceView, cachedCurrentTopologyNodes, freshNodes) => ( + (isResourceView ? cachedCurrentTopologyNodes : freshNodes) + .valueSeq() + .flatMap(n => n.get('metrics', makeList())) + .map(m => makeMap({ id: m.get('id'), label: m.get('label') })) + .toSet() + .toList() + .sortBy(m => m.get('label')) + ) +); export const pinnedMetricSelector = createSelector( [ - state => state.get('availableCanvasMetrics'), + availableMetricsSelector, state => state.get('pinnedMetricType'), ], - (availableMetrics, pinnedMetricType) => ( - availableMetrics.find(metric => metric.get('label') === pinnedMetricType) - ) + (availableMetrics, pinnedMetricType) => { + const metric = availableMetrics.find(m => m.get('label') === pinnedMetricType); + return metric && metric.get('id'); + } ); const topCardNodeSelector = createSelector( diff --git a/client/app/scripts/selectors/resource-view/layers.js b/client/app/scripts/selectors/resource-view/layers.js index d7073947f6..997a533977 100644 --- a/client/app/scripts/selectors/resource-view/layers.js +++ b/client/app/scripts/selectors/resource-view/layers.js @@ -112,7 +112,7 @@ export const positionedNodesByTopologySelector = createSelector( bucket.forEach((_, nodeId) => { const node = result.getIn([layerTopologyId, nodeId]); result = result.mergeIn([layerTopologyId, nodeId], makeMap({ - x: ((node.get('offset') - parentOffset) / overhead) + parentOffset, + offset: ((node.get('offset') - parentOffset) / overhead) + parentOffset, width: node.get('width') / overhead, })); }); diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index 6393914ec9..40eb621f49 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -1,5 +1,12 @@ import { createSelector } from 'reselect'; -import { RESOURCE_VIEW_MODE, GRAPH_VIEW_MODE, TABLE_VIEW_MODE } from '../constants/naming'; +import { Map as makeMap } from 'immutable'; + +import { + RESOURCE_VIEW_MODE, + GRAPH_VIEW_MODE, + TABLE_VIEW_MODE, +} from '../constants/naming'; + // TODO: Consider moving more stuff from 'topology-utils' here. @@ -24,6 +31,14 @@ export const isResourceViewModeSelector = createSelector( viewMode => viewMode === RESOURCE_VIEW_MODE ); +export const cachedCurrentTopologyNodesSelector = createSelector( + [ + state => state.get('nodesByTopology'), + state => state.get('currentTopologyId'), + ], + (nodesByTopology, currentTopologyId) => nodesByTopology.get(currentTopologyId, makeMap()) +); + // Checks if graph complexity is high. Used to trigger // table view on page load and decide on animations. export const graphExceedsComplexityThreshSelector = createSelector( diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js index 2d3d2564f7..de5b0d126d 100644 --- a/client/app/scripts/utils/topology-utils.js +++ b/client/app/scripts/utils/topology-utils.js @@ -1,6 +1,8 @@ import { endsWith } from 'lodash'; import { Set as makeSet, List as makeList } from 'immutable'; +import { isResourceViewModeSelector } from '../selectors/topology'; +import { pinnedMetricSelector } from '../selectors/node-metric'; // // top priority first @@ -132,8 +134,10 @@ export function getCurrentTopologyOptions(state) { } export function isTopologyEmpty(state) { - return state.getIn(['currentTopology', 'stats', 'node_count'], 0) === 0 - && state.get('nodes').size === 0; + const resourceViewEmpty = isResourceViewModeSelector(state) && !pinnedMetricSelector(state); + const nodeCount = state.getIn(['currentTopology', 'stats', 'node_count'], 0); + const nodesEmpty = nodeCount === 0 && state.get('nodes').size === 0; + return resourceViewEmpty || nodesEmpty; } diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 77dd7035e9..70fecf18dd 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -183,12 +183,16 @@ export function getAllNodes(getState, dispatch) { getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions); } +// Update the nodes for all topologies that appear in the current resource view. // NOTE: At the moment we are only getting their one-time snapshot (instead of polling), // because we intentionally want to keep the resource view layout static. Later on, we // will probably want to change this. export function getResourceViewNodesSnapshot(getState, dispatch) { const topologyIds = layersTopologyIdsSelector(getState()); - getNodesForTopologies(getState, dispatch, topologyIds); + // TODO: Find a more elegant way of fetching the topologies information. + setTimeout(() => { + getNodesForTopologies(getState, dispatch, topologyIds); + }, 100); } export function getTopologies(options, dispatch, initialPoll) { diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 5415f72355..a8e3fc0e07 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -1181,7 +1181,7 @@ } .error { - animation: blinking 2.0s 60 $base-ease; // blink for 2 minute; + animation: blinking 2.0s 60 $base-ease; // blink for 2 minutes color: $text-secondary-color; } @@ -1256,6 +1256,14 @@ } } +.metric-selector-message { + animation: blinking 1.0s infinite $base-ease; + color: $text-tertiary-color; + display: inline-block; + margin: 0; + padding: 0 10px; +} + .view-mode-selector { margin-top: 8px; margin-left: 20px; From 2373b712b163bdbdcd60f9bba8f85edb6bab0f35 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 21 Mar 2017 12:08:00 +0100 Subject: [PATCH 29/32] Separated zoom limits from the zoom active state --- client/app/scripts/charts/nodes-chart.js | 6 +- .../app/scripts/components/nodes-resources.js | 6 +- ...chable-zoom-wrapper.js => zoom-wrapper.js} | 88 +++++++++---------- .../selectors/graph-view/default-zoom.js | 61 +++++++++---- .../scripts/selectors/graph-view/layout.js | 6 +- .../selectors/resource-view/default-zoom.js | 67 ++++++++++---- client/app/scripts/selectors/zooming.js | 32 +++++-- 7 files changed, 172 insertions(+), 94 deletions(-) rename client/app/scripts/components/{cachable-zoom-wrapper.js => zoom-wrapper.js} (68%) diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 0dd9457754..7a51e4d9c4 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import Logo from '../components/logo'; import NodesChartElements from './nodes-chart-elements'; -import CachableZoomWrapper from '../components/cachable-zoom-wrapper'; +import ZoomWrapper from '../components/zoom-wrapper'; import { clickBackground } from '../actions/app-actions'; @@ -47,9 +47,9 @@ class NodesChart extends React.Component { - + - +
); diff --git a/client/app/scripts/components/nodes-resources.js b/client/app/scripts/components/nodes-resources.js index aeecaf548c..6ca74a416a 100644 --- a/client/app/scripts/components/nodes-resources.js +++ b/client/app/scripts/components/nodes-resources.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import Logo from './logo'; import { layersTopologyIdsSelector } from '../selectors/resource-view/layers'; -import CachableZoomWrapper from './cachable-zoom-wrapper'; +import ZoomWrapper from './zoom-wrapper'; import NodesResourcesLayer from './nodes-resources/node-resources-layer'; @@ -24,9 +24,9 @@ class NodesResources extends React.Component {
- + {transform => this.renderLayers(transform)} - +
); diff --git a/client/app/scripts/components/cachable-zoom-wrapper.js b/client/app/scripts/components/zoom-wrapper.js similarity index 68% rename from client/app/scripts/components/cachable-zoom-wrapper.js rename to client/app/scripts/components/zoom-wrapper.js index 0804a8edc9..9d904b9a30 100644 --- a/client/app/scripts/components/cachable-zoom-wrapper.js +++ b/client/app/scripts/components/zoom-wrapper.js @@ -8,8 +8,11 @@ import { zoom, zoomIdentity } from 'd3-zoom'; import { cacheZoomState } from '../actions/app-actions'; import { transformToString } from '../utils/transform-utils'; -import { activeLayoutZoomSelector } from '../selectors/zooming'; import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/topology'; +import { + activeLayoutZoomStateSelector, + activeLayoutZoomLimitsSelector, +} from '../selectors/zooming'; import { canvasMarginsSelector, canvasWidthSelector, @@ -19,7 +22,7 @@ import { import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer'; -class CachableZoomWrapper extends React.Component { +class ZoomWrapper extends React.Component { constructor(props, context) { super(props, context); @@ -46,7 +49,8 @@ class CachableZoomWrapper extends React.Component { this.svg = select(`svg#${this.props.svg}`); this.setZoomTriggers(!this.props.disabled); - this.restoreCachedZoom(this.props); + this.updateZoomLimits(this.props); + this.restoreZoomState(this.props); } componentWillUnmount() { @@ -71,8 +75,9 @@ class CachableZoomWrapper extends React.Component { this.setZoomTriggers(!nextProps.disabled); } + this.updateZoomLimits(nextProps); if (!this.zoomRestored) { - this.restoreCachedZoom(nextProps); + this.restoreZoomState(nextProps); } } @@ -102,20 +107,10 @@ class CachableZoomWrapper extends React.Component { // Decides which part of the zoom state is cachable depending // on the horizontal/vertical degrees of freedom. cachableState(state = this.state) { - // TODO: Probably shouldn't cache the limits if the layout can - // change a lot. However, before removing them from here, we have - // to make sure we can always get them from the default zooms. - let cachableFields = [ - 'minTranslateX', 'maxTranslateX', - 'minTranslateY', 'maxTranslateY', - 'minScale', 'maxScale' - ]; - if (!this.props.fixHorizontal) { - cachableFields = cachableFields.concat(['scaleX', 'translateX']); - } - if (!this.props.fixVertical) { - cachableFields = cachableFields.concat(['scaleY', 'translateY']); - } + const cachableFields = [] + .concat(this.props.fixHorizontal ? [] : ['scaleX', 'translateX']) + .concat(this.props.fixVertical ? [] : ['scaleY', 'translateY']); + return pick(state, cachableFields); } @@ -123,31 +118,35 @@ class CachableZoomWrapper extends React.Component { this.props.cacheZoomState(fromJS(this.cachableState())); } + updateZoomLimits(props) { + const zoomLimits = props.layoutZoomLimits.toJS(); + + this.zoom = this.zoom.scaleExtent([zoomLimits.minScale, zoomLimits.maxScale]); + + if (props.bounded) { + this.zoom = this.zoom + // Translation limits are only set if explicitly demanded (currently we are using them + // in the resource view, but not in the graph view, although I think the idea would be + // to use them everywhere). + .translateExtent([ + [zoomLimits.minTranslateX, zoomLimits.minTranslateY], + [zoomLimits.maxTranslateX, zoomLimits.maxTranslateY], + ]) + // This is to ensure that the translation limits are properly + // centered, so that the canvas margins are respected. + .extent([ + [props.canvasMargins.left, props.canvasMargins.top], + [props.canvasMargins.left + props.width, props.canvasMargins.top + props.height] + ]); + } + + this.setState(zoomLimits); + } + // Restore the zooming settings - restoreCachedZoom(props) { - if (!props.layoutZoom.isEmpty()) { - const zoomState = props.layoutZoom.toJS(); - - // Scaling limits are always set. - this.zoom = this.zoom.scaleExtent([zoomState.minScale, zoomState.maxScale]); - - // Translation limits are optional. - if (props.bounded) { - this.zoom = this.zoom - // Translation limits are only set if explicitly demanded (currently we are using them - // in the resource view, but not in the graph view, although I think the idea would be - // to use them everywhere). - .translateExtent([ - [zoomState.minTranslateX, zoomState.minTranslateY], - [zoomState.maxTranslateX, zoomState.maxTranslateY], - ]) - // This is to ensure that the translation limits are properly - // centered, so that the canvas margins are respected. - .extent([ - [props.canvasMargins.left, props.canvasMargins.top], - [props.canvasMargins.left + props.width, props.canvasMargins.top + props.height] - ]); - } + restoreZoomState(props) { + if (!props.layoutZoomState.isEmpty()) { + const zoomState = props.layoutZoomState.toJS(); // After the limits have been set, update the zoom. this.svg.call(this.zoom.transform, zoomIdentity @@ -181,7 +180,8 @@ function mapStateToProps(state) { width: canvasWidthSelector(state), height: canvasHeightSelector(state), canvasMargins: canvasMarginsSelector(state), - layoutZoom: activeLayoutZoomSelector(state), + layoutZoomState: activeLayoutZoomStateSelector(state), + layoutZoomLimits: activeLayoutZoomLimitsSelector(state), layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)), forceRelayout: state.get('forceRelayout'), }; @@ -191,4 +191,4 @@ function mapStateToProps(state) { export default connect( mapStateToProps, { cacheZoomState } -)(CachableZoomWrapper); +)(ZoomWrapper); diff --git a/client/app/scripts/selectors/graph-view/default-zoom.js b/client/app/scripts/selectors/graph-view/default-zoom.js index b0dd34696d..e6521e3e95 100644 --- a/client/app/scripts/selectors/graph-view/default-zoom.js +++ b/client/app/scripts/selectors/graph-view/default-zoom.js @@ -6,37 +6,51 @@ import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from import { graphNodesSelector } from './graph'; -// Compute the default zoom settings for the given graph. -export const graphDefaultZoomSelector = createSelector( +const graphBoundingRectangleSelector = createSelector( [ graphNodesSelector, - canvasMarginsSelector, - canvasWidthSelector, - canvasHeightSelector, ], - (graphNodes, canvasMargins, width, height) => { - if (graphNodes.size === 0) { - return makeMap(); - } + (graphNodes) => { + if (graphNodes.size === 0) return null; const xMin = graphNodes.map(n => n.get('x') - NODE_BASE_SIZE).min(); const yMin = graphNodes.map(n => n.get('y') - NODE_BASE_SIZE).min(); const xMax = graphNodes.map(n => n.get('x') + NODE_BASE_SIZE).max(); const yMax = graphNodes.map(n => n.get('y') + NODE_BASE_SIZE).max(); + return makeMap({ xMin, yMin, xMax, yMax }); + } +); + +// Max scale limit will always be such that a node covers 1/5 of the viewport. +const maxScaleSelector = createSelector( + [ + canvasWidthSelector, + canvasHeightSelector, + ], + (width, height) => Math.min(width, height) / NODE_BASE_SIZE / 5 +); + +// Compute the default zoom settings for the given graph. +export const graphDefaultZoomSelector = createSelector( + [ + graphBoundingRectangleSelector, + canvasMarginsSelector, + canvasWidthSelector, + canvasHeightSelector, + maxScaleSelector, + ], + (boundingRectangle, canvasMargins, width, height, maxScale) => { + if (!boundingRectangle) return makeMap(); + + const { xMin, xMax, yMin, yMax } = boundingRectangle.toJS(); const xFactor = width / (xMax - xMin); const yFactor = height / (yMax - yMin); - // Maximal allowed zoom will always be such that a node covers 1/5 of the viewport. - const maxScale = Math.min(width, height) / NODE_BASE_SIZE / 5; - // Initial zoom is such that the graph covers 90% of either the viewport, // or one half of maximal zoom constraint, whichever is smaller. const scale = Math.min(xFactor, yFactor, maxScale / 2) * 0.9; - // Finally, we always allow zooming out exactly 5x compared to the initial zoom. - const minScale = scale / 5; - // This translation puts the graph in the center of the viewport, respecting the margins. const translateX = ((width - ((xMax + xMin) * scale)) / 2) + canvasMargins.left; const translateY = ((height - ((yMax + yMin) * scale)) / 2) + canvasMargins.top; @@ -44,10 +58,23 @@ export const graphDefaultZoomSelector = createSelector( return makeMap({ translateX, translateY, - minScale, - maxScale, scaleX: scale, scaleY: scale, }); } ); + +export const graphZoomLimitsSelector = createSelector( + [ + graphDefaultZoomSelector, + maxScaleSelector, + ], + (defaultZoom, maxScale) => { + if (defaultZoom.isEmpty()) return makeMap({ minScale: 1, maxScale: 1 }); + + // We always allow zooming out exactly 5x compared to the initial zoom. + const minScale = defaultZoom.get('scaleX') / 5; + + return makeMap({ minScale, maxScale }); + } +); diff --git a/client/app/scripts/selectors/graph-view/layout.js b/client/app/scripts/selectors/graph-view/layout.js index 52172f79ba..a8f3fe40ba 100644 --- a/client/app/scripts/selectors/graph-view/layout.js +++ b/client/app/scripts/selectors/graph-view/layout.js @@ -5,7 +5,7 @@ import { fromJS, Set as makeSet, List as makeList } from 'immutable'; import { NODE_BASE_SIZE } from '../../constants/styles'; import { graphNodesSelector, graphEdgesSelector } from './graph'; -import { activeLayoutZoomSelector } from '../zooming'; +import { activeLayoutZoomStateSelector } from '../zooming'; import { canvasCircularExpanseSelector, canvasDetailsHorizontalCenterSelector, @@ -25,7 +25,7 @@ const translationToViewportCenterSelector = createSelector( [ canvasDetailsHorizontalCenterSelector, canvasDetailsVerticalCenterSelector, - activeLayoutZoomSelector, + activeLayoutZoomStateSelector, ], (centerX, centerY, zoomState) => { const { scaleX, scaleY, translateX, translateY } = zoomState.toJS(); @@ -76,7 +76,7 @@ const focusedNodesIdsSelector = createSelector( const circularLayoutScalarsSelector = createSelector( [ // TODO: Fix this. - state => activeLayoutZoomSelector(state).get('scaleX'), + state => activeLayoutZoomStateSelector(state).get('scaleX'), state => focusedNodesIdsSelector(state).length - 1, canvasCircularExpanseSelector, ], diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index 9dff5f3778..2ec356146f 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -6,19 +6,13 @@ import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from import { layersVerticalPositionSelector, positionedNodesByTopologySelector } from './layers'; -// Compute the default zoom settings for the given chart. -export const resourcesDefaultZoomSelector = createSelector( +const resourcesBoundingRectangleSelector = createSelector( [ layersVerticalPositionSelector, positionedNodesByTopologySelector, - canvasMarginsSelector, - canvasWidthSelector, - canvasHeightSelector, ], - (verticalPositions, nodes, canvasMargins, width, height) => { - if (nodes.size === 0) { - return makeMap(); - } + (verticalPositions, nodes) => { + if (nodes.size === 0) return null; const flattenedNodes = nodes.flatten(true); const xMin = flattenedNodes.map(n => n.get('offset')).min(); @@ -26,28 +20,65 @@ export const resourcesDefaultZoomSelector = createSelector( const xMax = flattenedNodes.map(n => n.get('offset') + n.get('width')).max(); const yMax = verticalPositions.toList().max() + RESOURCES_LAYER_HEIGHT; - const minNodeWidth = flattenedNodes.map(n => n.get('width')).min(); + return makeMap({ xMin, xMax, yMin, yMax }); + } +); + +// Compute the default zoom settings for the given chart. +export const resourcesDefaultZoomSelector = createSelector( + [ + resourcesBoundingRectangleSelector, + canvasMarginsSelector, + canvasWidthSelector, + canvasHeightSelector, + ], + (boundingRectangle, canvasMargins, width, height) => { + if (!boundingRectangle) return makeMap(); + + const { xMin, xMax, yMin, yMax } = boundingRectangle.toJS(); const scaleX = (width / (xMax - xMin)) * 1.0; const scaleY = (height / (yMax - yMin)) * 0.7; - const maxScale = width / minNodeWidth; - const minScale = scaleX; // This translation puts the graph in the center of the viewport, respecting the margins. const translateX = ((width - ((xMax + xMin) * scaleX)) / 2) + canvasMargins.left; const translateY = ((height - ((yMax + yMin) * scaleY)) / 2) + canvasMargins.top; return makeMap({ - minTranslateX: xMin, - maxTranslateX: xMax, - minTranslateY: yMin, - maxTranslateY: yMax, translateX, translateY, - minScale, - maxScale, scaleX, scaleY, }); } ); + +const minNodeWidthSelector = createSelector( + [ + positionedNodesByTopologySelector, + ], + nodes => nodes.flatten(true).map(n => n.get('width')).min() +); + +export const resourcesZoomLimitsSelector = createSelector( + [ + resourcesDefaultZoomSelector, + resourcesBoundingRectangleSelector, + minNodeWidthSelector, + canvasWidthSelector, + ], + (defaultZoom, boundingRectangle, minNodeWidth, width) => { + if (defaultZoom.isEmpty()) return makeMap(); + + const { xMin, xMax, yMin, yMax } = boundingRectangle.toJS(); + + return makeMap({ + maxScale: width / minNodeWidth, + minScale: defaultZoom.get('scaleX'), + minTranslateX: xMin, + maxTranslateX: xMax, + minTranslateY: yMin, + maxTranslateY: yMax, + }); + } +); diff --git a/client/app/scripts/selectors/zooming.js b/client/app/scripts/selectors/zooming.js index 3b782fef03..62588bed84 100644 --- a/client/app/scripts/selectors/zooming.js +++ b/client/app/scripts/selectors/zooming.js @@ -1,9 +1,18 @@ import { createSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; -import { graphDefaultZoomSelector } from './graph-view/default-zoom'; -import { resourcesDefaultZoomSelector } from './resource-view/default-zoom'; -import { activeTopologyZoomCacheKeyPathSelector, isGraphViewModeSelector } from './topology'; +import { + graphZoomLimitsSelector, + graphDefaultZoomSelector, +} from './graph-view/default-zoom'; +import { + resourcesZoomLimitsSelector, + resourcesDefaultZoomSelector, +} from './resource-view/default-zoom'; +import { + activeTopologyZoomCacheKeyPathSelector, + isGraphViewModeSelector, +} from './topology'; const activeLayoutCachedZoomSelector = createSelector( @@ -14,14 +23,25 @@ const activeLayoutCachedZoomSelector = createSelector( (zoomCache, keyPath) => zoomCache.getIn(keyPath.slice(1), makeMap()) ); -export const activeLayoutZoomSelector = createSelector( +export const activeLayoutZoomLimitsSelector = createSelector( + [ + isGraphViewModeSelector, + graphZoomLimitsSelector, + resourcesZoomLimitsSelector, + ], + (isGraphView, graphZoomLimits, resourcesZoomLimits) => ( + isGraphView ? graphZoomLimits : resourcesZoomLimits + ) +); + +export const activeLayoutZoomStateSelector = createSelector( [ - activeLayoutCachedZoomSelector, isGraphViewModeSelector, graphDefaultZoomSelector, resourcesDefaultZoomSelector, + activeLayoutCachedZoomSelector, ], - (cachedZoomState, isGraphView, graphDefaultZoom, resourcesDefaultZoom) => { + (isGraphView, graphDefaultZoom, resourcesDefaultZoom, cachedZoomState) => { const defaultZoom = isGraphView ? graphDefaultZoom : resourcesDefaultZoom; return defaultZoom.merge(cachedZoomState); } From 8fec8a35d88868cbdfdf4c5e5e69e7c520e971a8 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 21 Mar 2017 17:28:52 +0100 Subject: [PATCH 30/32] Renaming and bunch of comments --- .../app/scripts/components/nodes-resources.js | 2 +- .../nodes-resources/node-resources-layer.js | 19 +- .../node-resources-metric-box-info.js | 10 +- .../node-resources-metric-box.js | 11 +- .../scripts/components/view-mode-selector.js | 2 +- client/app/scripts/constants/resources.js | 8 +- client/app/scripts/constants/styles.js | 13 -- client/app/scripts/decorators/node.js | 61 +++--- .../selectors/graph-view/default-zoom.js | 2 +- .../scripts/selectors/graph-view/layout.js | 1 - .../selectors/resource-view/default-zoom.js | 41 ++-- .../scripts/selectors/resource-view/layers.js | 125 ------------- .../scripts/selectors/resource-view/layout.js | 177 ++++++++++++++++++ client/app/scripts/selectors/topology.js | 6 + client/app/scripts/selectors/zooming.js | 1 + client/app/scripts/utils/metric-utils.js | 1 + client/app/scripts/utils/topology-utils.js | 2 + client/app/scripts/utils/web-api-utils.js | 17 +- 18 files changed, 283 insertions(+), 216 deletions(-) delete mode 100644 client/app/scripts/selectors/resource-view/layers.js create mode 100644 client/app/scripts/selectors/resource-view/layout.js diff --git a/client/app/scripts/components/nodes-resources.js b/client/app/scripts/components/nodes-resources.js index 6ca74a416a..25e076225f 100644 --- a/client/app/scripts/components/nodes-resources.js +++ b/client/app/scripts/components/nodes-resources.js @@ -2,9 +2,9 @@ import React from 'react'; import { connect } from 'react-redux'; import Logo from './logo'; -import { layersTopologyIdsSelector } from '../selectors/resource-view/layers'; import ZoomWrapper from './zoom-wrapper'; import NodesResourcesLayer from './nodes-resources/node-resources-layer'; +import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; class NodesResources extends React.Component { 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 1fa26f3f94..af613cac57 100644 --- a/client/app/scripts/components/nodes-resources/node-resources-layer.js +++ b/client/app/scripts/components/nodes-resources/node-resources-layer.js @@ -5,25 +5,24 @@ import { Map as makeMap } from 'immutable'; import NodeResourcesMetricBox from './node-resources-metric-box'; import NodeResourcesLayerTopology from './node-resources-layer-topology'; import { - layersVerticalPositionSelector, - positionedNodesByTopologySelector, -} from '../../selectors/resource-view/layers'; + layerVerticalPositionByTopologyIdSelector, + layoutNodesByTopologyIdSelector, +} from '../../selectors/resource-view/layout'; class NodesResourcesLayer extends React.Component { render() { - const { layerVerticalPosition, topologyId, transform, nodes } = this.props; + const { layerVerticalPosition, topologyId, transform, layoutNodes } = this.props; return ( - {nodes.toIndexedSeq().map(node => ( + {layoutNodes.toIndexedSeq().map(node => ( ))} - {!nodes.isEmpty() && = RESOURCES_LABEL_MIN_SIZE; - const showNode = width >= 1; // px + const showNode = width >= 1; // hide the thin nodes // Don't display the nodes which are less than 1px wide. // TODO: Show `+ 31 nodes` kind of tag in their stead. @@ -82,12 +82,11 @@ class NodeResourcesMetricBox extends React.Component { return ( - {info} - {withCapacity && } + {showCapacity && } {showInfo && m.get('label') === metricType); - if (!metric) return node; - - const { formattedValue } = getMetricValue(metric); - const info = `${metricType} - ${formattedValue}`; - const absoluteConsumption = metric.get('value'); - const withCapacity = node.get('withCapacity'); - const totalCapacity = withCapacity ? metric.get('max') : absoluteConsumption; - const relativeConsumption = absoluteConsumption / totalCapacity; - const format = metric.get('format'); - - return node.set('activeMetric', makeMap({ - totalCapacity, absoluteConsumption, relativeConsumption, withCapacity, info, format - })); -} - +// Decorates the resource node with dimensions taken from its metric summary. export function nodeResourceBoxDecorator(node) { - const widthCriterion = node.get('withCapacity') ? 'totalCapacity' : 'absoluteConsumption'; - const width = node.getIn(['activeMetric', widthCriterion]); + const metricSummary = node.get('metricSummary', makeMap()); + const width = metricSummary.get('showCapacity') ? + metricSummary.get('totalCapacity') : + metricSummary.get('absoluteConsumption'); const height = RESOURCES_LAYER_HEIGHT; return node.merge(makeMap({ width, height })); } -export function nodeParentNodeDecorator(node) { - const parentTopologyId = node.get('directParentTopologyId'); - const parents = node.get('parents', makeMap()); - const parent = parents.find(p => p.get('topologyId') === parentTopologyId); - if (!parent) return node; +// Decorates the node with the summary info of its metric of a fixed type. +export function nodeMetricSummaryDecoratorByType(metricType, showCapacity) { + return (node) => { + const metric = node + .get('metrics', makeMap()) + .find(m => m.get('label') === metricType); + + // Do nothing if there is no metric info. + if (!metric) return node; + + const absoluteConsumption = metric.get('value'); + const totalCapacity = showCapacity ? metric.get('max') : absoluteConsumption; + const relativeConsumption = absoluteConsumption / totalCapacity; + const format = metric.get('format'); + + return node.set('metricSummary', makeMap({ + showCapacity, totalCapacity, absoluteConsumption, relativeConsumption, format + })); + }; +} + +// Decorates the node with the ID of the parent node belonging to a fixed topology. +export function nodeParentDecoratorByTopologyId(topologyId) { + return (node) => { + const parent = node + .get('parents', makeMap()) + .find(p => p.get('topologyId') === topologyId); - return node.set('parentNodeId', parent.get('id')); + return parent ? node.set('parentNodeId', parent.get('id')) : node; + }; } diff --git a/client/app/scripts/selectors/graph-view/default-zoom.js b/client/app/scripts/selectors/graph-view/default-zoom.js index e6521e3e95..039196b0fd 100644 --- a/client/app/scripts/selectors/graph-view/default-zoom.js +++ b/client/app/scripts/selectors/graph-view/default-zoom.js @@ -70,7 +70,7 @@ export const graphZoomLimitsSelector = createSelector( maxScaleSelector, ], (defaultZoom, maxScale) => { - if (defaultZoom.isEmpty()) return makeMap({ minScale: 1, maxScale: 1 }); + if (defaultZoom.isEmpty()) return makeMap(); // We always allow zooming out exactly 5x compared to the initial zoom. const minScale = defaultZoom.get('scaleX') / 5; diff --git a/client/app/scripts/selectors/graph-view/layout.js b/client/app/scripts/selectors/graph-view/layout.js index a8f3fe40ba..cbbb0bc9f5 100644 --- a/client/app/scripts/selectors/graph-view/layout.js +++ b/client/app/scripts/selectors/graph-view/layout.js @@ -75,7 +75,6 @@ const focusedNodesIdsSelector = createSelector( const circularLayoutScalarsSelector = createSelector( [ - // TODO: Fix this. state => activeLayoutZoomStateSelector(state).get('scaleX'), state => focusedNodesIdsSelector(state).length - 1, canvasCircularExpanseSelector, diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index 2ec356146f..6f0b12b0de 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -3,18 +3,29 @@ import { Map as makeMap } from 'immutable'; import { RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from '../canvas'; -import { layersVerticalPositionSelector, positionedNodesByTopologySelector } from './layers'; +import { + layerVerticalPositionByTopologyIdSelector, + layoutNodesByTopologyIdSelector, +} from './layout'; -const resourcesBoundingRectangleSelector = createSelector( +// This is used to determine the maximal zoom factor. +const minNodeWidthSelector = createSelector( + [ + layoutNodesByTopologyIdSelector, + ], + layoutNodes => layoutNodes.flatten(true).map(n => n.get('width')).min() +); + +const resourceNodesBoundingRectangleSelector = createSelector( [ - layersVerticalPositionSelector, - positionedNodesByTopologySelector, + layerVerticalPositionByTopologyIdSelector, + layoutNodesByTopologyIdSelector, ], - (verticalPositions, nodes) => { - if (nodes.size === 0) return null; + (verticalPositions, layoutNodes) => { + if (layoutNodes.size === 0) return null; - const flattenedNodes = nodes.flatten(true); + const flattenedNodes = layoutNodes.flatten(true); const xMin = flattenedNodes.map(n => n.get('offset')).min(); const yMin = verticalPositions.toList().min(); const xMax = flattenedNodes.map(n => n.get('offset') + n.get('width')).max(); @@ -24,10 +35,10 @@ const resourcesBoundingRectangleSelector = createSelector( } ); -// Compute the default zoom settings for the given chart. +// Compute the default zoom settings for given resources. export const resourcesDefaultZoomSelector = createSelector( [ - resourcesBoundingRectangleSelector, + resourceNodesBoundingRectangleSelector, canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector, @@ -37,6 +48,7 @@ export const resourcesDefaultZoomSelector = createSelector( const { xMin, xMax, yMin, yMax } = boundingRectangle.toJS(); + // The default scale takes all the available horizontal space and 70% of the vertical space. const scaleX = (width / (xMax - xMin)) * 1.0; const scaleY = (height / (yMax - yMin)) * 0.7; @@ -53,17 +65,10 @@ export const resourcesDefaultZoomSelector = createSelector( } ); -const minNodeWidthSelector = createSelector( - [ - positionedNodesByTopologySelector, - ], - nodes => nodes.flatten(true).map(n => n.get('width')).min() -); - export const resourcesZoomLimitsSelector = createSelector( [ resourcesDefaultZoomSelector, - resourcesBoundingRectangleSelector, + resourceNodesBoundingRectangleSelector, minNodeWidthSelector, canvasWidthSelector, ], @@ -73,7 +78,9 @@ export const resourcesZoomLimitsSelector = createSelector( const { xMin, xMax, yMin, yMax } = boundingRectangle.toJS(); return makeMap({ + // Maximal zoom is such that the smallest box takes the whole canvas. maxScale: width / minNodeWidth, + // Minimal zoom is equivalent to the initial one, where the whole layout matches the canvas. minScale: defaultZoom.get('scaleX'), minTranslateX: xMin, maxTranslateX: xMax, diff --git a/client/app/scripts/selectors/resource-view/layers.js b/client/app/scripts/selectors/resource-view/layers.js deleted file mode 100644 index 997a533977..0000000000 --- a/client/app/scripts/selectors/resource-view/layers.js +++ /dev/null @@ -1,125 +0,0 @@ -import { times } from 'lodash'; -import { fromJS, Map as makeMap } from 'immutable'; -import { createSelector } from 'reselect'; - -import { RESOURCES_LAYER_PADDING, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; -import { resourceViewLayers, topologiesWithCapacity } from '../../constants/resources'; -import { - nodeResourceViewColorDecorator, - nodeParentNodeDecorator, - nodeResourceBoxDecorator, - nodeActiveMetricDecorator, -} from '../../decorators/node'; - - -const RESOURCE_VIEW_MAX_LAYERS = 3; - -const nodeWeight = node => ( - node.get('withCapacity') ? - -node.getIn(['activeMetric', 'relativeConsumption']) : - -node.get('width') -); - -export const layersTopologyIdsSelector = createSelector( - [ - state => state.get('currentTopologyId'), - ], - topologyId => fromJS(resourceViewLayers[topologyId] || []) -); - -export const layersVerticalPositionSelector = createSelector( - [ - layersTopologyIdsSelector, - ], - (topologiesIds) => { - let yPositions = makeMap(); - let currentY = RESOURCES_LAYER_PADDING; - - topologiesIds.forEach((topologyId) => { - currentY -= RESOURCES_LAYER_HEIGHT + RESOURCES_LAYER_PADDING; - yPositions = yPositions.set(topologyId, currentY); - }); - - return yPositions; - } -); - -const decoratedNodesByTopologySelector = createSelector( - [ - layersTopologyIdsSelector, - state => state.get('pinnedMetricType'), - ...times(RESOURCE_VIEW_MAX_LAYERS, index => ( - state => state.getIn(['nodesByTopology', layersTopologyIdsSelector(state).get(index)]) - )) - ], - (layersTopologyIds, pinnedMetricType, ...topologiesNodes) => { - let nodesByTopology = makeMap(); - let lastLayerTopologyId = null; - - topologiesNodes.forEach((topologyNodes, index) => { - const layerTopologyId = layersTopologyIds.get(index); - const withCapacity = topologiesWithCapacity.includes(layerTopologyId); - const decoratedTopologyNodes = (topologyNodes || makeMap()) - .map(node => node.set('directParentTopologyId', lastLayerTopologyId)) - .map(node => node.set('topologyId', layerTopologyId)) - .map(node => node.set('activeMetricType', pinnedMetricType)) - .map(node => node.set('withCapacity', withCapacity)) - .map(nodeResourceViewColorDecorator) - .map(nodeActiveMetricDecorator) - .map(nodeResourceBoxDecorator) - .map(nodeParentNodeDecorator); - const filteredTopologyNodes = decoratedTopologyNodes - .filter(node => nodesByTopology.hasIn([lastLayerTopologyId, node.get('parentNodeId')]) - || index === 0) - .filter(node => node.get('width')); - - nodesByTopology = nodesByTopology.set(layerTopologyId, filteredTopologyNodes); - lastLayerTopologyId = layerTopologyId; - }); - - return nodesByTopology; - } -); - -export const positionedNodesByTopologySelector = createSelector( - [ - layersTopologyIdsSelector, - decoratedNodesByTopologySelector, - ], - (layersTopologyIds, decoratedNodesByTopology) => { - let result = makeMap(); - - layersTopologyIds.forEach((layerTopologyId, index) => { - const decoratedNodes = decoratedNodesByTopology.get(layerTopologyId, makeMap()); - const buckets = decoratedNodes.groupBy(node => node.get('parentNodeId')); - - buckets.forEach((bucket, parentNodeId) => { - const parentTopologyId = layersTopologyIds.get(index - 1); - let offset = result.getIn([parentTopologyId, parentNodeId, 'offset'], 0); - - bucket.sortBy(nodeWeight).forEach((node, nodeId) => { - const positionedNode = node.set('offset', offset); - result = result.setIn([layerTopologyId, nodeId], positionedNode); - offset += node.get('width'); - }); - - // TODO: Get rid of this disgusting code - const parentOffset = result.getIn([parentTopologyId, parentNodeId, 'offset'], 0); - const parentWidth = result.getIn([parentTopologyId, parentNodeId, 'width'], offset); - const overhead = (offset - parentOffset) / parentWidth; - if (overhead > 1) { - // console.log(overhead); - bucket.forEach((_, nodeId) => { - const node = result.getIn([layerTopologyId, nodeId]); - result = result.mergeIn([layerTopologyId, nodeId], makeMap({ - offset: ((node.get('offset') - parentOffset) / overhead) + parentOffset, - width: node.get('width') / overhead, - })); - }); - } - }); - }); - - return result; - } -); diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js new file mode 100644 index 0000000000..021c8f3f30 --- /dev/null +++ b/client/app/scripts/selectors/resource-view/layout.js @@ -0,0 +1,177 @@ +import debug from 'debug'; +import { times } from 'lodash'; +import { fromJS, Map as makeMap } from 'immutable'; +import { createSelector } from 'reselect'; + +import { RESOURCES_LAYER_PADDING, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; +import { + RESOURCE_VIEW_MAX_LAYERS, + RESOURCE_VIEW_LAYERS, + TOPOLOGIES_WITH_CAPACITY, +} from '../../constants/resources'; +import { + nodeParentDecoratorByTopologyId, + nodeMetricSummaryDecoratorByType, + nodeResourceViewColorDecorator, + nodeResourceBoxDecorator, +} from '../../decorators/node'; + + +const log = debug('scope:nodes-layout'); + +// Used for ordering the resource nodes. +const resourceNodeConsumptionComparator = (node) => { + const metricSummary = node.get('metricSummary'); + return metricSummary.get('showCapacity') ? + -metricSummary.get('relativeConsumption') : + -metricSummary.get('absoluteConsumption'); +}; + +// A list of topologies shown in the resource view of the active topology (bottom to top). +export const layersTopologyIdsSelector = createSelector( + [ + state => state.get('currentTopologyId'), + ], + topologyId => fromJS(RESOURCE_VIEW_LAYERS[topologyId] || []) +); + +// Calculates the resource view layer Y-coordinate for every topology in the resource view. +export const layerVerticalPositionByTopologyIdSelector = createSelector( + [ + layersTopologyIdsSelector, + ], + (topologiesIds) => { + let yPositions = makeMap(); + let currentY = RESOURCES_LAYER_PADDING; + + topologiesIds.forEach((topologyId) => { + currentY -= RESOURCES_LAYER_HEIGHT + RESOURCES_LAYER_PADDING; + yPositions = yPositions.set(topologyId, currentY); + }); + + return yPositions; + } +); + +// Decorate and filter all the nodes to be displayed in the current resource view, except +// for the exact node horizontal offsets which are calculated from the data created here. +const decoratedNodesByTopologySelector = createSelector( + [ + layersTopologyIdsSelector, + state => state.get('pinnedMetricType'), + // Generate the dependencies for this selector programmatically (because we want their + // number to be customizable directly by changing the constant). The dependency functions + // here depend on another selector, but this seems to work quite fine. For example, if + // layersTopologyIdsSelector = ['hosts', 'containers'] and RESOURCE_VIEW_MAX_LAYERS = 3, + // this code will generate: + // [ + // state => state.getIn(['nodesByTopology', 'hosts']) + // state => state.getIn(['nodesByTopology', 'containers']) + // state => state.getIn(['nodesByTopology', undefined]) + // ] + // which will all be captured by `topologiesNodes` and processed correctly (even for undefined). + ...times(RESOURCE_VIEW_MAX_LAYERS, index => ( + state => state.getIn(['nodesByTopology', layersTopologyIdsSelector(state).get(index)]) + )) + ], + (layersTopologyIds, pinnedMetricType, ...topologiesNodes) => { + let nodesByTopology = makeMap(); + let parentLayerTopologyId = null; + + topologiesNodes.forEach((topologyNodes, index) => { + const layerTopologyId = layersTopologyIds.get(index); + const parentTopologyNodes = nodesByTopology.get(parentLayerTopologyId, makeMap()); + const showCapacity = TOPOLOGIES_WITH_CAPACITY.includes(layerTopologyId); + const isBaseLayer = (index === 0); + + const nodeParentDecorator = nodeParentDecoratorByTopologyId(parentLayerTopologyId); + const nodeMetricSummaryDecorator = nodeMetricSummaryDecoratorByType( + pinnedMetricType, showCapacity); + + // Color the node, deduce its anchor point, dimensions and info about its pinned metric. + const decoratedTopologyNodes = (topologyNodes || makeMap()) + .map(nodeResourceViewColorDecorator) + .map(nodeMetricSummaryDecorator) + .map(nodeResourceBoxDecorator) + .map(nodeParentDecorator); + + const filteredTopologyNodes = decoratedTopologyNodes + // Filter out the nodes with no parent in the topology of the previous layer, as their + // positions in the layout could not be determined. The exception is the base layer. + // TODO: Also make an exception for uncontained nodes (e.g. processes). + .filter(node => parentTopologyNodes.has(node.get('parentNodeId')) || isBaseLayer) + // Filter out the nodes with no metric summary data, which is needed to render the node. + .filter(node => node.get('metricSummary')); + + nodesByTopology = nodesByTopology.set(layerTopologyId, filteredTopologyNodes); + parentLayerTopologyId = layerTopologyId; + }); + + return nodesByTopology; + } +); + +// Calculate (and fix) the offsets for all the displayed resource nodes. +export const layoutNodesByTopologyIdSelector = createSelector( + [ + layersTopologyIdsSelector, + decoratedNodesByTopologySelector, + ], + (layersTopologyIds, nodesByTopology) => { + let layoutNodes = makeMap(); + let parentTopologyId = null; + + // Calculate the offsets bottom-to top as each layer needs to know exact offsets of its parents. + layersTopologyIds.forEach((layerTopologyId) => { + let positionedNodes = makeMap(); + + // Get the nodes in the current layer grouped by their parent nodes. + // Each of those buckets will be positioned and sorted independently. + const nodesByParent = nodesByTopology + .get(layerTopologyId, makeMap()) + .groupBy(n => n.get('parentNodeId')); + + nodesByParent.forEach((nodesBucket, parentNodeId) => { + // Set the initial offset to the offset of the parent (that has already been set). + // If there is no offset information, i.e. we're processing the base layer, set it to 0. + const parentNode = layoutNodes.getIn([parentTopologyId, parentNodeId], makeMap()); + let currentOffset = parentNode.get('offset', 0); + + // Sort the nodes in the current bucket and lay them down one after another. + nodesBucket.sortBy(resourceNodeConsumptionComparator).forEach((node, nodeId) => { + const positionedNode = node.set('offset', currentOffset); + positionedNodes = positionedNodes.set(nodeId, positionedNode); + currentOffset += node.get('width'); + }); + + // TODO: This block of code checks for the overlaps which are caused by children + // consuming more resources than their parent node. This happens due to inconsistent + // data being sent from the backend and it needs to be fixed there. + const parentOffset = parentNode.get('offset', 0); + const parentWidth = parentNode.get('width', currentOffset); + const totalChildrenWidth = currentOffset - parentOffset; + // If the total width of the children exceeds the parent node box width, we have a problem. + // We fix it by shrinking all the children to by a factor to perfectly fit into the parent. + if (totalChildrenWidth > parentWidth) { + const shrinkFactor = parentWidth / totalChildrenWidth; + log(`Inconsistent data: Children of ${parentNodeId} reported to use more ` + + `resource than the node itself - shrinking by factor ${shrinkFactor}`); + // Shrink all the children. + nodesBucket.forEach((_, nodeId) => { + const node = positionedNodes.get(nodeId); + positionedNodes = positionedNodes.mergeIn([nodeId], makeMap({ + offset: ((node.get('offset') - parentOffset) * shrinkFactor) + parentOffset, + width: node.get('width') * shrinkFactor, + })); + }); + } + }); + + // Update the layout with the positioned node from the current layer. + layoutNodes = layoutNodes.mergeIn([layerTopologyId], positionedNodes); + parentTopologyId = layerTopologyId; + }); + + return layoutNodes; + } +); diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index 40eb621f49..31dce70fa5 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -31,6 +31,9 @@ export const isResourceViewModeSelector = createSelector( viewMode => viewMode === RESOURCE_VIEW_MODE ); +// This is used by the resource view where we're always taking the nodes from the cache, +// so that polling doesn't affect the layout. Once we implement a more robust polling +// mechanism that could poll multiple topologies at once, we'll be able to get rid of this. export const cachedCurrentTopologyNodesSelector = createSelector( [ state => state.get('nodesByTopology'), @@ -71,7 +74,10 @@ export const activeTopologyZoomCacheKeyPathSelector = createSelector( ], (isGraphViewMode, viewMode, topologyId, pinnedMetricType, topologyOptions) => ( isGraphViewMode ? + // In graph view, selecting different options/filters produces a different layout. ['zoomCache', viewMode, topologyId, topologyOptions] : + // Otherwise we're in the resource view where the options are hidden (for now), + // but pinning different metrics can result in very different layouts. ['zoomCache', viewMode, topologyId, pinnedMetricType] ) ); diff --git a/client/app/scripts/selectors/zooming.js b/client/app/scripts/selectors/zooming.js index 62588bed84..cf011e05d2 100644 --- a/client/app/scripts/selectors/zooming.js +++ b/client/app/scripts/selectors/zooming.js @@ -43,6 +43,7 @@ export const activeLayoutZoomStateSelector = createSelector( ], (isGraphView, graphDefaultZoom, resourcesDefaultZoom, cachedZoomState) => { const defaultZoom = isGraphView ? graphDefaultZoom : resourcesDefaultZoom; + // All the cached fields override the calculated default ones. return defaultZoom.merge(cachedZoomState); } ); diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js index f9232a5583..e67852a3f5 100644 --- a/client/app/scripts/utils/metric-utils.js +++ b/client/app/scripts/utils/metric-utils.js @@ -54,6 +54,7 @@ export function getMetricValue(metric) { }; } + export function getMetricColor(metric) { const selectedMetric = metric && metric.get('id'); if (/mem/.test(selectedMetric)) { diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js index de5b0d126d..82cf1f65c8 100644 --- a/client/app/scripts/utils/topology-utils.js +++ b/client/app/scripts/utils/topology-utils.js @@ -134,7 +134,9 @@ export function getCurrentTopologyOptions(state) { } export function isTopologyEmpty(state) { + // Consider a topology in the resource view empty if it has no pinned metric. const resourceViewEmpty = isResourceViewModeSelector(state) && !pinnedMetricSelector(state); + // Otherwise (in graph and table view), we only look at the node count. const nodeCount = state.getIn(['currentTopology', 'stats', 'node_count'], 0); const nodesEmpty = nodeCount === 0 && state.get('nodes').size === 0; return resourceViewEmpty || nodesEmpty; diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 70fecf18dd..83f1104099 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -10,7 +10,7 @@ import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveEr receiveControlSuccess, receiveTopologies, receiveNotFound, receiveNodesForTopology } from '../actions/app-actions'; -import { layersTopologyIdsSelector } from '../selectors/resource-view/layers'; +import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; import { API_INTERVAL, TOPOLOGY_INTERVAL } from '../constants/timer'; const log = debug('scope:web-api-utils'); @@ -158,6 +158,9 @@ function doRequest(opts) { return reqwest(config); } +/** + * Does a one-time fetch of all the nodes for a custom list of topologies. + */ function getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions = makeMap()) { // fetch sequentially getState().get('topologyUrlsById') @@ -174,7 +177,7 @@ function getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions } /** - * Gets nodes for all topologies (for search) + * Gets nodes for all topologies (for search). */ export function getAllNodes(getState, dispatch) { const state = getState(); @@ -183,13 +186,13 @@ export function getAllNodes(getState, dispatch) { getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions); } -// Update the nodes for all topologies that appear in the current resource view. -// NOTE: At the moment we are only getting their one-time snapshot (instead of polling), -// because we intentionally want to keep the resource view layout static. Later on, we -// will probably want to change this. +/** + * One-time update of all the nodes of topologies that appear in the current resource view. + */ export function getResourceViewNodesSnapshot(getState, dispatch) { const topologyIds = layersTopologyIdsSelector(getState()); - // TODO: Find a more elegant way of fetching the topologies information. + // TODO: Replace this with polling once we figure how to make resource view dynamic + // (from the UI point of view, the challenge is to make it stable). setTimeout(() => { getNodesForTopologies(getState, dispatch, topologyIds); }, 100); From 3dc4e9d0ff06986b1f54cc351960353b86c55767 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 22 Mar 2017 18:46:28 +0100 Subject: [PATCH 31/32] Addressed all the UI comments (@davkal + @fons) --- client/app/scripts/actions/app-actions.js | 1 - client/app/scripts/components/app.js | 6 +-- .../app/scripts/components/metric-selector.js | 9 +---- .../node-resources-metric-box-info.js | 17 +++----- .../node-resources-metric-box.js | 7 +++- .../scripts/components/view-mode-selector.js | 4 +- client/app/scripts/constants/resources.js | 19 ++++++++- client/app/scripts/decorators/node.js | 13 ++++++- .../app/scripts/selectors/graph-view/graph.js | 2 +- client/app/scripts/selectors/node-metric.js | 39 ++++++++++++------- client/app/scripts/selectors/topology.js | 16 ++------ client/app/scripts/utils/web-api-utils.js | 6 +-- client/app/styles/_base.scss | 8 ---- 13 files changed, 79 insertions(+), 68 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 22b2fc2aed..fd33701fb4 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -368,7 +368,6 @@ export function clickResumeUpdate() { function updateTopology(dispatch, getState) { const state = getState(); // If we're in the resource view, get the snapshot of all the relevant node topologies. - // TODO: Consider updating the state to always have a pinned metric. if (isResourceViewModeSelector(state)) { getResourceViewNodesSnapshot(getState, dispatch); } diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index a1fd46f0e4..de82efc600 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -17,11 +17,9 @@ import { focusSearch, pinNextMetric, hitBackspace, hitEnter, hitEsc, unpinMetric import Details from './details'; import Nodes from './nodes'; import ViewModeSelector from './view-mode-selector'; -import MetricSelector from './metric-selector'; import NetworkSelector from './networks-selector'; import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar'; import { getRouter, getUrlState } from '../utils/router-utils'; -import { availableMetricsSelector } from '../selectors/node-metric'; import { availableNetworksSelector } from '../selectors/node-networks'; import { activeTopologyOptionsSelector, @@ -110,7 +108,7 @@ class App extends React.Component { render() { const { isTableViewMode, isGraphViewMode, isResourceViewMode, showingDetails, showingHelp, - showingMetricsSelector, showingNetworkSelector, showingTroubleshootingMenu } = this.props; + showingNetworkSelector, showingTroubleshootingMenu } = this.props; const isIframe = window !== window.top; return ( @@ -137,7 +135,6 @@ class App extends React.Component { - {showingMetricsSelector && isGraphViewMode && } {showingNetworkSelector && isGraphViewMode && } {!isResourceViewMode && } {!isResourceViewMode && } @@ -162,7 +159,6 @@ function mapStateToProps(state) { showingDetails: state.get('nodeDetails').size > 0, showingHelp: state.get('showingHelp'), showingTroubleshootingMenu: state.get('showingTroubleshootingMenu'), - showingMetricsSelector: availableMetricsSelector(state).count() > 0, showingNetworkSelector: availableNetworksSelector(state).count() > 0, showingTerminal: state.get('controlPipes').size > 0, urlState: getUrlState(state) diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index c77657286f..e17bfbab9c 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { selectMetric } from '../actions/app-actions'; -import { availableMetricsSelector, pinnedMetricSelector } from '../selectors/node-metric'; +import { availableMetricsSelector } from '../selectors/node-metric'; import MetricSelectorItem from './metric-selector-item'; class MetricSelector extends React.Component { @@ -17,8 +17,7 @@ class MetricSelector extends React.Component { } render() { - const { pinnedMetric, alwaysPinned, availableMetrics } = this.props; - const shouldPinMetric = alwaysPinned && !pinnedMetric && !availableMetrics.isEmpty(); + const { alwaysPinned, availableMetrics } = this.props; return (
@@ -31,9 +30,6 @@ class MetricSelector extends React.Component { /> ))}
- {shouldPinMetric && - « Select a metric - }
); } @@ -42,7 +38,6 @@ class MetricSelector extends React.Component { function mapStateToProps(state) { return { availableMetrics: availableMetricsSelector(state), - pinnedMetric: pinnedMetricSelector(state), }; } 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 e3656deff7..e2bbfb5a6b 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,24 +1,19 @@ import React from 'react'; -import { formatMetricSvg } from '../../utils/string-utils'; - export default class NodeResourcesMetricBoxInfo extends React.Component { humanizedMetricInfo() { - const metricSummary = this.props.metricSummary.toJS(); - const showExtendedInfo = metricSummary.showCapacity && metricSummary.format !== 'percent'; - const totalCapacity = formatMetricSvg(metricSummary.totalCapacity, metricSummary); - const absoluteConsumption = formatMetricSvg(metricSummary.absoluteConsumption, metricSummary); - const relativeConsumption = formatMetricSvg(100.0 * metricSummary.relativeConsumption, - { format: 'percent' }); + const { humanizedTotalCapacity, humanizedAbsoluteConsumption, + humanizedRelativeConsumption, showCapacity, format } = this.props.metricSummary.toJS(); + const showExtendedInfo = showCapacity && format !== 'percent'; return ( - {showExtendedInfo ? relativeConsumption : absoluteConsumption} - consumed + {showExtendedInfo ? humanizedRelativeConsumption : humanizedAbsoluteConsumption} + used {showExtendedInfo && {' - '} - ({absoluteConsumption} / {totalCapacity}) + ({humanizedAbsoluteConsumption} / {humanizedTotalCapacity}) } ); 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 aac65a724f..9fc6e10745 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 @@ -71,7 +71,7 @@ class NodeResourcesMetricBox extends React.Component { render() { const { x, y, width } = this.state; const { label, color, metricSummary } = this.props; - const { showCapacity, relativeConsumption } = metricSummary.toJS(); + const { showCapacity, relativeConsumption, type } = metricSummary.toJS(); const showInfo = width >= RESOURCES_LABEL_MIN_SIZE; const showNode = width >= 1; // hide the thin nodes @@ -80,8 +80,13 @@ class NodeResourcesMetricBox extends React.Component { // TODO: Show `+ 31 nodes` kind of tag in their stead. if (!showNode) return null; + const resourceUsageTooltipInfo = showCapacity ? + metricSummary.get('humanizedRelativeConsumption') : + metricSummary.get('humanizedAbsoluteConsumption'); + return ( + {label} - {type} usage at {resourceUsageTooltipInfo} {showCapacity && } {showInfo && - {isResourceViewMode && } +
); } @@ -57,6 +58,7 @@ function mapStateToProps(state) { isTableViewMode: isTableViewModeSelector(state), isResourceViewMode: isResourceViewModeSelector(state), hasResourceView: !layersTopologyIdsSelector(state).isEmpty(), + showingMetricsSelector: availableMetricsSelector(state).count() > 0, }; } diff --git a/client/app/scripts/constants/resources.js b/client/app/scripts/constants/resources.js index 29648fac9d..848be6ad51 100644 --- a/client/app/scripts/constants/resources.js +++ b/client/app/scripts/constants/resources.js @@ -5,8 +5,23 @@ export const RESOURCE_VIEW_MAX_LAYERS = 3; // TODO: Consider fetching these from the backend. export const TOPOLOGIES_WITH_CAPACITY = ['hosts']; + +// TODO: These too should ideally be provided by the backend. Currently, we are showing +// the same layers for all the topologies, because their number is small, but later on +// we might be interested in fully customizing the layers' hierarchy per topology. export const RESOURCE_VIEW_LAYERS = { hosts: ['hosts', 'containers', 'processes'], - containers: ['containers', 'processes'], - processes: ['processes'], + containers: ['hosts', 'containers', 'processes'], + processes: ['hosts', 'containers', 'processes'], }; + +// TODO: These are all the common metrics that appear across all the current resource view +// topologies. The reason for taking them only is that we want to get meaningful data for all +// the layers. These should be taken directly from the backend report, but as their info is +// currently only contained in the nodes data, it would be hard to determine them before all +// the nodes for all the layers have been loaded, so we'd need to change the routing logic +// since the requirement is that one these is always pinned in the resource view. +export const RESOURCE_VIEW_METRICS = [ + { label: 'CPU', id: 'host_cpu_usage_percent' }, + { label: 'Memory', id: 'host_mem_usage_bytes' }, +]; diff --git a/client/app/scripts/decorators/node.js b/client/app/scripts/decorators/node.js index 93cd4d6aa9..f4d1a65176 100644 --- a/client/app/scripts/decorators/node.js +++ b/client/app/scripts/decorators/node.js @@ -1,6 +1,7 @@ import { Map as makeMap } from 'immutable'; import { getNodeColor } from '../utils/color-utils'; +import { formatMetricSvg } from '../utils/string-utils'; import { RESOURCES_LAYER_HEIGHT } from '../constants/styles'; @@ -34,10 +35,20 @@ export function nodeMetricSummaryDecoratorByType(metricType, showCapacity) { const absoluteConsumption = metric.get('value'); const totalCapacity = showCapacity ? metric.get('max') : absoluteConsumption; const relativeConsumption = absoluteConsumption / totalCapacity; + const defaultMetric = { format: metric.get('format') }; + const percentMetric = { format: 'percent' }; const format = metric.get('format'); return node.set('metricSummary', makeMap({ - showCapacity, totalCapacity, absoluteConsumption, relativeConsumption, format + showCapacity, + type: metricType, + humanizedTotalCapacity: formatMetricSvg(totalCapacity, defaultMetric), + humanizedAbsoluteConsumption: formatMetricSvg(absoluteConsumption, defaultMetric), + humanizedRelativeConsumption: formatMetricSvg(100 * relativeConsumption, percentMetric), + totalCapacity, + absoluteConsumption, + relativeConsumption, + format, })); }; } diff --git a/client/app/scripts/selectors/graph-view/graph.js b/client/app/scripts/selectors/graph-view/graph.js index 37c9c74f57..72783d9e04 100644 --- a/client/app/scripts/selectors/graph-view/graph.js +++ b/client/app/scripts/selectors/graph-view/graph.js @@ -7,7 +7,7 @@ import { canvasWidthSelector, canvasHeightSelector } from '../canvas'; import { activeTopologyOptionsSelector } from '../topology'; import { shownNodesSelector } from '../node-filters'; import { doLayout } from '../../charts/nodes-layout'; -import timer from '../utils/timer-utils'; +import timer from '../../utils/timer-utils'; const log = debug('scope:nodes-chart'); diff --git a/client/app/scripts/selectors/node-metric.js b/client/app/scripts/selectors/node-metric.js index 7f54ce7a57..8c17c9cede 100644 --- a/client/app/scripts/selectors/node-metric.js +++ b/client/app/scripts/selectors/node-metric.js @@ -2,10 +2,8 @@ import { createSelector } from 'reselect'; import { createMapSelector } from 'reselect-map'; import { fromJS, Map as makeMap, List as makeList } from 'immutable'; -import { - isResourceViewModeSelector, - cachedCurrentTopologyNodesSelector, -} from '../selectors/topology'; +import { isGraphViewModeSelector, isResourceViewModeSelector } from '../selectors/topology'; +import { RESOURCE_VIEW_METRICS } from '../constants/resources'; // Resource view uses the metrics of the nodes from the cache, while the graph and table @@ -13,19 +11,32 @@ import { // options which are currently ignored in the resource view). export const availableMetricsSelector = createSelector( [ + isGraphViewModeSelector, isResourceViewModeSelector, - cachedCurrentTopologyNodesSelector, state => state.get('nodes'), ], - (isResourceView, cachedCurrentTopologyNodes, freshNodes) => ( - (isResourceView ? cachedCurrentTopologyNodes : freshNodes) - .valueSeq() - .flatMap(n => n.get('metrics', makeList())) - .map(m => makeMap({ id: m.get('id'), label: m.get('label') })) - .toSet() - .toList() - .sortBy(m => m.get('label')) - ) + (isGraphView, isResourceView, nodes) => { + // In graph view, we always look through the fresh state + // of topology nodes to get all the available metrics. + if (isGraphView) { + return nodes + .valueSeq() + .flatMap(n => n.get('metrics', makeList())) + .map(m => makeMap({ id: m.get('id'), label: m.get('label') })) + .toSet() + .toList() + .sortBy(m => m.get('label')); + } + + // In resource view, we're displaying only the hardcoded CPU and Memory metrics. + // TODO: Make this dynamic as well. + if (isResourceView) { + return fromJS(RESOURCE_VIEW_METRICS); + } + + // Don't show any metrics in the table view mode. + return makeList(); + } ); export const pinnedMetricSelector = createSelector( diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index 31dce70fa5..35e6b63fd9 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -1,5 +1,4 @@ import { createSelector } from 'reselect'; -import { Map as makeMap } from 'immutable'; import { RESOURCE_VIEW_MODE, @@ -31,17 +30,6 @@ export const isResourceViewModeSelector = createSelector( viewMode => viewMode === RESOURCE_VIEW_MODE ); -// This is used by the resource view where we're always taking the nodes from the cache, -// so that polling doesn't affect the layout. Once we implement a more robust polling -// mechanism that could poll multiple topologies at once, we'll be able to get rid of this. -export const cachedCurrentTopologyNodesSelector = createSelector( - [ - state => state.get('nodesByTopology'), - state => state.get('currentTopologyId'), - ], - (nodesByTopology, currentTopologyId) => nodesByTopology.get(currentTopologyId, makeMap()) -); - // Checks if graph complexity is high. Used to trigger // table view on page load and decide on animations. export const graphExceedsComplexityThreshSelector = createSelector( @@ -78,6 +66,8 @@ export const activeTopologyZoomCacheKeyPathSelector = createSelector( ['zoomCache', viewMode, topologyId, topologyOptions] : // Otherwise we're in the resource view where the options are hidden (for now), // but pinning different metrics can result in very different layouts. - ['zoomCache', viewMode, topologyId, pinnedMetricType] + // TODO: Take `topologyId` into account once the resource + // view layouts start differing between the topologies. + ['zoomCache', viewMode, pinnedMetricType] ) ); diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 83f1104099..0e24baa21c 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -191,11 +191,11 @@ export function getAllNodes(getState, dispatch) { */ export function getResourceViewNodesSnapshot(getState, dispatch) { const topologyIds = layersTopologyIdsSelector(getState()); - // TODO: Replace this with polling once we figure how to make resource view dynamic - // (from the UI point of view, the challenge is to make it stable). + // TODO: Remove the timeout and replace it with normal polling once we figure how to make + // resource view dynamic (from the UI point of view, the challenge is to make it stable). setTimeout(() => { getNodesForTopologies(getState, dispatch, topologyIds); - }, 100); + }, 1200); } export function getTopologies(options, dispatch, initialPoll) { diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index a8e3fc0e07..8a20987082 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -1256,14 +1256,6 @@ } } -.metric-selector-message { - animation: blinking 1.0s infinite $base-ease; - color: $text-tertiary-color; - display: inline-block; - margin: 0; - padding: 0 10px; -} - .view-mode-selector { margin-top: 8px; margin-left: 20px; From ca3f74fa01e7ac47b4a87e16da62b8345a49a10c Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 24 Mar 2017 12:50:12 +0100 Subject: [PATCH 32/32] Made graph view selectors independent from resource view selectors --- client/app/scripts/charts/nodes-chart.js | 10 +++- .../app/scripts/components/nodes-resources.js | 9 +++- client/app/scripts/components/zoom-wrapper.js | 12 ++--- client/app/scripts/reducers/root.js | 2 +- .../scripts/selectors/graph-view/layout.js | 6 +-- .../graph-view/{default-zoom.js => zoom.js} | 10 ++++ .../{default-zoom.js => zoom.js} | 10 ++++ client/app/scripts/selectors/topology.js | 20 ------- client/app/scripts/selectors/zooming.js | 54 +++++++------------ 9 files changed, 63 insertions(+), 70 deletions(-) rename client/app/scripts/selectors/graph-view/{default-zoom.js => zoom.js} (88%) rename client/app/scripts/selectors/resource-view/{default-zoom.js => zoom.js} (89%) diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 7a51e4d9c4..8a60a924ac 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -5,7 +5,10 @@ import Logo from '../components/logo'; import NodesChartElements from './nodes-chart-elements'; import ZoomWrapper from '../components/zoom-wrapper'; import { clickBackground } from '../actions/app-actions'; - +import { + graphZoomLimitsSelector, + graphZoomStateSelector, +} from '../selectors/graph-view/zoom'; const EdgeMarkerDefinition = ({ selectedNodeId }) => { const markerOffset = selectedNodeId ? '35' : '40'; @@ -47,7 +50,10 @@ class NodesChart extends React.Component { - + diff --git a/client/app/scripts/components/nodes-resources.js b/client/app/scripts/components/nodes-resources.js index 25e076225f..6638341343 100644 --- a/client/app/scripts/components/nodes-resources.js +++ b/client/app/scripts/components/nodes-resources.js @@ -5,6 +5,10 @@ import Logo from './logo'; import ZoomWrapper from './zoom-wrapper'; import NodesResourcesLayer from './nodes-resources/node-resources-layer'; import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; +import { + resourcesZoomLimitsSelector, + resourcesZoomStateSelector, +} from '../selectors/resource-view/zoom'; class NodesResources extends React.Component { @@ -24,7 +28,10 @@ class NodesResources extends React.Component {
- + {transform => this.renderLayers(transform)} diff --git a/client/app/scripts/components/zoom-wrapper.js b/client/app/scripts/components/zoom-wrapper.js index 9d904b9a30..8fb82f392e 100644 --- a/client/app/scripts/components/zoom-wrapper.js +++ b/client/app/scripts/components/zoom-wrapper.js @@ -8,11 +8,7 @@ import { zoom, zoomIdentity } from 'd3-zoom'; import { cacheZoomState } from '../actions/app-actions'; import { transformToString } from '../utils/transform-utils'; -import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/topology'; -import { - activeLayoutZoomStateSelector, - activeLayoutZoomLimitsSelector, -} from '../selectors/zooming'; +import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming'; import { canvasMarginsSelector, canvasWidthSelector, @@ -175,13 +171,13 @@ class ZoomWrapper extends React.Component { } -function mapStateToProps(state) { +function mapStateToProps(state, props) { return { width: canvasWidthSelector(state), height: canvasHeightSelector(state), canvasMargins: canvasMarginsSelector(state), - layoutZoomState: activeLayoutZoomStateSelector(state), - layoutZoomLimits: activeLayoutZoomLimitsSelector(state), + layoutZoomState: props.zoomStateSelector(state), + layoutZoomLimits: props.zoomLimitsSelector(state), layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)), forceRelayout: state.get('forceRelayout'), }; diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index bc837d5fad..7221a1d41e 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -12,9 +12,9 @@ import { } from '../constants/naming'; import { graphExceedsComplexityThreshSelector, - activeTopologyZoomCacheKeyPathSelector, isResourceViewModeSelector, } from '../selectors/topology'; +import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming'; import { availableMetricsSelector, pinnedMetricSelector } from '../selectors/node-metric'; import { applyPinnedSearches } from '../utils/search-utils'; import { diff --git a/client/app/scripts/selectors/graph-view/layout.js b/client/app/scripts/selectors/graph-view/layout.js index cbbb0bc9f5..399564f5a1 100644 --- a/client/app/scripts/selectors/graph-view/layout.js +++ b/client/app/scripts/selectors/graph-view/layout.js @@ -5,7 +5,7 @@ import { fromJS, Set as makeSet, List as makeList } from 'immutable'; import { NODE_BASE_SIZE } from '../../constants/styles'; import { graphNodesSelector, graphEdgesSelector } from './graph'; -import { activeLayoutZoomStateSelector } from '../zooming'; +import { graphZoomStateSelector } from './zoom'; import { canvasCircularExpanseSelector, canvasDetailsHorizontalCenterSelector, @@ -25,7 +25,7 @@ const translationToViewportCenterSelector = createSelector( [ canvasDetailsHorizontalCenterSelector, canvasDetailsVerticalCenterSelector, - activeLayoutZoomStateSelector, + graphZoomStateSelector, ], (centerX, centerY, zoomState) => { const { scaleX, scaleY, translateX, translateY } = zoomState.toJS(); @@ -75,7 +75,7 @@ const focusedNodesIdsSelector = createSelector( const circularLayoutScalarsSelector = createSelector( [ - state => activeLayoutZoomStateSelector(state).get('scaleX'), + state => graphZoomStateSelector(state).get('scaleX'), state => focusedNodesIdsSelector(state).length - 1, canvasCircularExpanseSelector, ], diff --git a/client/app/scripts/selectors/graph-view/default-zoom.js b/client/app/scripts/selectors/graph-view/zoom.js similarity index 88% rename from client/app/scripts/selectors/graph-view/default-zoom.js rename to client/app/scripts/selectors/graph-view/zoom.js index 039196b0fd..fcb084ef6a 100644 --- a/client/app/scripts/selectors/graph-view/default-zoom.js +++ b/client/app/scripts/selectors/graph-view/zoom.js @@ -3,6 +3,7 @@ import { Map as makeMap } from 'immutable'; import { NODE_BASE_SIZE } from '../../constants/styles'; import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from '../canvas'; +import { activeLayoutCachedZoomSelector } from '../zooming'; import { graphNodesSelector } from './graph'; @@ -78,3 +79,12 @@ export const graphZoomLimitsSelector = createSelector( return makeMap({ minScale, maxScale }); } ); + +export const graphZoomStateSelector = createSelector( + [ + graphDefaultZoomSelector, + activeLayoutCachedZoomSelector, + ], + // All the cached fields override the calculated default ones. + (graphDefaultZoom, cachedZoomState) => graphDefaultZoom.merge(cachedZoomState) +); diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/zoom.js similarity index 89% rename from client/app/scripts/selectors/resource-view/default-zoom.js rename to client/app/scripts/selectors/resource-view/zoom.js index 6f0b12b0de..39b43d55dd 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/zoom.js @@ -3,6 +3,7 @@ import { Map as makeMap } from 'immutable'; import { RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from '../canvas'; +import { activeLayoutCachedZoomSelector } from '../zooming'; import { layerVerticalPositionByTopologyIdSelector, layoutNodesByTopologyIdSelector, @@ -89,3 +90,12 @@ export const resourcesZoomLimitsSelector = createSelector( }); } ); + +export const resourcesZoomStateSelector = createSelector( + [ + resourcesDefaultZoomSelector, + activeLayoutCachedZoomSelector, + ], + // All the cached fields override the calculated default ones. + (resourcesDefaultZoom, cachedZoomState) => resourcesDefaultZoom.merge(cachedZoomState) +); diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index 35e6b63fd9..30b39fea7b 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -51,23 +51,3 @@ export const activeTopologyOptionsSelector = createSelector( topologyOptions.get(parentTopologyId || currentTopologyId) ) ); - -export const activeTopologyZoomCacheKeyPathSelector = createSelector( - [ - isGraphViewModeSelector, - state => state.get('topologyViewMode'), - state => state.get('currentTopologyId'), - state => state.get('pinnedMetricType'), - state => JSON.stringify(activeTopologyOptionsSelector(state)), - ], - (isGraphViewMode, viewMode, topologyId, pinnedMetricType, topologyOptions) => ( - isGraphViewMode ? - // In graph view, selecting different options/filters produces a different layout. - ['zoomCache', viewMode, topologyId, topologyOptions] : - // Otherwise we're in the resource view where the options are hidden (for now), - // but pinning different metrics can result in very different layouts. - // TODO: Take `topologyId` into account once the resource - // view layouts start differing between the topologies. - ['zoomCache', viewMode, pinnedMetricType] - ) -); diff --git a/client/app/scripts/selectors/zooming.js b/client/app/scripts/selectors/zooming.js index cf011e05d2..8f8922bf41 100644 --- a/client/app/scripts/selectors/zooming.js +++ b/client/app/scripts/selectors/zooming.js @@ -1,49 +1,33 @@ import { createSelector } from 'reselect'; import { Map as makeMap } from 'immutable'; -import { - graphZoomLimitsSelector, - graphDefaultZoomSelector, -} from './graph-view/default-zoom'; -import { - resourcesZoomLimitsSelector, - resourcesDefaultZoomSelector, -} from './resource-view/default-zoom'; -import { - activeTopologyZoomCacheKeyPathSelector, - isGraphViewModeSelector, -} from './topology'; +import { isGraphViewModeSelector, activeTopologyOptionsSelector } from './topology'; -const activeLayoutCachedZoomSelector = createSelector( - [ - state => state.get('zoomCache'), - activeTopologyZoomCacheKeyPathSelector, - ], - (zoomCache, keyPath) => zoomCache.getIn(keyPath.slice(1), makeMap()) -); - -export const activeLayoutZoomLimitsSelector = createSelector( +export const activeTopologyZoomCacheKeyPathSelector = createSelector( [ isGraphViewModeSelector, - graphZoomLimitsSelector, - resourcesZoomLimitsSelector, + state => state.get('topologyViewMode'), + state => state.get('currentTopologyId'), + state => state.get('pinnedMetricType'), + state => JSON.stringify(activeTopologyOptionsSelector(state)), ], - (isGraphView, graphZoomLimits, resourcesZoomLimits) => ( - isGraphView ? graphZoomLimits : resourcesZoomLimits + (isGraphViewMode, viewMode, topologyId, pinnedMetricType, topologyOptions) => ( + isGraphViewMode ? + // In graph view, selecting different options/filters produces a different layout. + ['zoomCache', viewMode, topologyId, topologyOptions] : + // Otherwise we're in the resource view where the options are hidden (for now), + // but pinning different metrics can result in very different layouts. + // TODO: Take `topologyId` into account once the resource + // view layouts start differing between the topologies. + ['zoomCache', viewMode, pinnedMetricType] ) ); -export const activeLayoutZoomStateSelector = createSelector( +export const activeLayoutCachedZoomSelector = createSelector( [ - isGraphViewModeSelector, - graphDefaultZoomSelector, - resourcesDefaultZoomSelector, - activeLayoutCachedZoomSelector, + state => state.get('zoomCache'), + activeTopologyZoomCacheKeyPathSelector, ], - (isGraphView, graphDefaultZoom, resourcesDefaultZoom, cachedZoomState) => { - const defaultZoom = isGraphView ? graphDefaultZoom : resourcesDefaultZoom; - // All the cached fields override the calculated default ones. - return defaultZoom.merge(cachedZoomState); - } + (zoomCache, keyPath) => zoomCache.getIn(keyPath.slice(1), makeMap()) );