From 69fd397217d02982b2ce24cd8a3fddef26f89e27 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 24 Mar 2017 14:51:53 +0100 Subject: [PATCH] Initial version of the resource view (#2296) * Added resource view selector button * Showing resource boxes in the resource view * Crude CPU resource view prototype * Improved the viewMode state logic * Extracted zooming into a separate wrapper component * Split the layout selectors between graph-view and resource-view * Proper zooming logic for the resource view * Moved all node networks utils to selectors * Improved the zoom caching logic * Further refactoring of selectors * Added sticky labels to the resource boxes * Added panning translation limits in the resource view * Renamed GridModeSelector -> ViewModeSelector * Polished the topology resource view selection logic * Search bar hidden in the resource view * Added per-layer topology names to the resource view * Made metric selectors work for the resource view * Adjusted the viewport selectors * Renamed viewport selector to canvas (+ maximal zoom fix) * Showing more useful metric info in the resource box labels * Fetching only necessary nodes for the resource view * Refactored the resource view layer component * Addressed first batch UI comments (from the Scope meeting) * Switch to deep zooming transform in the resource view to avoid SVG precision errors * Renamed and moved resource view components * Polished all the resource view components * Changing the available metrics selection * Improved and polished the state transition logic for the resource view * Separated zoom limits from the zoom active state * Renaming and bunch of comments * Addressed all the UI comments (@davkal + @fons) * Made graph view selectors independent from resource view selectors --- client/app/scripts/actions/app-actions.js | 113 +++++++---- .../scripts/charts/nodes-chart-elements.js | 6 +- client/app/scripts/charts/nodes-chart.js | 178 ++++------------ client/app/scripts/charts/nodes-grid.js | 14 +- client/app/scripts/components/app.js | 29 +-- .../app/scripts/components/debug-toolbar.js | 7 +- .../scripts/components/grid-mode-selector.js | 62 ------ client/app/scripts/components/help-panel.js | 7 +- client/app/scripts/components/logo.js | 4 +- .../components/metric-selector-item.js | 11 +- .../app/scripts/components/metric-selector.js | 20 +- .../app/scripts/components/nodes-resources.js | 51 +++++ .../node-resources-layer-topology.js | 31 +++ .../nodes-resources/node-resources-layer.js | 53 +++++ .../node-resources-metric-box-info.js | 33 +++ .../node-resources-metric-box.js | 111 ++++++++++ client/app/scripts/components/nodes.js | 27 ++- client/app/scripts/components/search.js | 11 +- client/app/scripts/components/topologies.js | 8 +- .../components/troubleshooting-menu.js | 12 +- .../scripts/components/view-mode-selector.js | 68 +++++++ client/app/scripts/components/zoom-wrapper.js | 190 ++++++++++++++++++ client/app/scripts/constants/action-types.js | 2 +- client/app/scripts/constants/naming.js | 6 + client/app/scripts/constants/resources.js | 27 +++ client/app/scripts/constants/styles.js | 25 ++- client/app/scripts/decorators/node.js | 65 ++++++ .../scripts/reducers/__tests__/root-test.js | 7 +- client/app/scripts/reducers/root.js | 77 ++++--- .../app/scripts/selectors/canvas-viewport.js | 48 ----- client/app/scripts/selectors/canvas.js | 63 ++++++ .../graph.js} | 16 +- .../layout.js} | 30 +-- .../app/scripts/selectors/graph-view/zoom.js | 90 +++++++++ client/app/scripts/selectors/node-metric.js | 49 ++++- client/app/scripts/selectors/node-networks.js | 39 ++-- .../app/scripts/selectors/nodes-chart-zoom.js | 64 ------ .../scripts/selectors/resource-view/layout.js | 177 ++++++++++++++++ .../scripts/selectors/resource-view/zoom.js | 101 ++++++++++ client/app/scripts/selectors/topology.js | 36 +++- client/app/scripts/selectors/zooming.js | 33 +++ client/app/scripts/utils/file-utils.js | 8 +- .../app/scripts/utils/network-view-utils.js | 15 -- client/app/scripts/utils/router-utils.js | 2 +- client/app/scripts/utils/topology-utils.js | 10 +- client/app/scripts/utils/transform-utils.js | 16 ++ client/app/scripts/utils/web-api-utils.js | 35 +++- client/app/styles/_base.scss | 71 +++++-- client/app/styles/_contrast-overrides.scss | 1 + client/app/styles/_variables.scss | 1 + 50 files changed, 1592 insertions(+), 568 deletions(-) delete mode 100644 client/app/scripts/components/grid-mode-selector.js create mode 100644 client/app/scripts/components/nodes-resources.js create mode 100644 client/app/scripts/components/nodes-resources/node-resources-layer-topology.js create mode 100644 client/app/scripts/components/nodes-resources/node-resources-layer.js create mode 100644 client/app/scripts/components/nodes-resources/node-resources-metric-box-info.js create mode 100644 client/app/scripts/components/nodes-resources/node-resources-metric-box.js create mode 100644 client/app/scripts/components/view-mode-selector.js create mode 100644 client/app/scripts/components/zoom-wrapper.js create mode 100644 client/app/scripts/constants/resources.js create mode 100644 client/app/scripts/decorators/node.js delete mode 100644 client/app/scripts/selectors/canvas-viewport.js create mode 100644 client/app/scripts/selectors/canvas.js rename client/app/scripts/selectors/{nodes-chart-graph.js => graph-view/graph.js} (84%) rename client/app/scripts/selectors/{nodes-chart-layout.js => graph-view/layout.js} (87%) create mode 100644 client/app/scripts/selectors/graph-view/zoom.js delete mode 100644 client/app/scripts/selectors/nodes-chart-zoom.js create mode 100644 client/app/scripts/selectors/resource-view/layout.js create mode 100644 client/app/scripts/selectors/resource-view/zoom.js create mode 100644 client/app/scripts/selectors/zooming.js delete mode 100644 client/app/scripts/utils/network-view-utils.js create mode 100644 client/app/scripts/utils/transform-utils.js diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 219979734..fd33701fb 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,7 +24,16 @@ 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 { availableMetricsSelector, pinnedMetricSelector } from '../selectors/node-metric'; +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'); @@ -141,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); @@ -248,25 +258,57 @@ 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 }); }; } -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 setTableView() { + return (dispatch, getState) => { + dispatch({ + 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, + }); + // 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); + }; +} + export function clickNode(nodeId, label, origin) { return (dispatch, getState) => { dispatch({ @@ -323,6 +365,25 @@ 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. + if (isResourceViewModeSelector(state)) { + 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 + // keep it than to redo the nodes delta updating logic. + getNodesDelta( + getCurrentTopologyUrl(state), + activeTopologyOptionsSelector(state), + dispatch + ); +} + export function clickShowTopologyForNode(topologyId, nodeId) { return (dispatch, getState) => { dispatch({ @@ -330,15 +391,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); }; } @@ -348,15 +401,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); }; } @@ -397,16 +442,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, @@ -700,6 +735,12 @@ export function route(urlState) { state.get('nodeDetails'), dispatch ); + // 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)) { + getResourceViewNodesSnapshot(getState, dispatch); + } }; } diff --git a/client/app/scripts/charts/nodes-chart-elements.js b/client/app/scripts/charts/nodes-chart-elements.js index 44f71e422..fb9e1eca4 100644 --- a/client/app/scripts/charts/nodes-chart-elements.js +++ b/client/app/scripts/charts/nodes-chart-elements.js @@ -8,15 +8,15 @@ import { selectedScaleSelector, layoutNodesSelector, layoutEdgesSelector -} from '../selectors/nodes-chart-layout'; +} from '../selectors/graph-view/layout'; class NodesChartElements extends React.Component { render() { - const { layoutNodes, layoutEdges, selectedScale, transform, isAnimated } = this.props; + const { layoutNodes, layoutEdges, selectedScale, isAnimated } = this.props; return ( - + { + const markerOffset = selectedNodeId ? '35' : '40'; + const markerSize = selectedNodeId ? '10' : '30'; + return ( + + + + + + ); +}; class NodesChart extends React.Component { constructor(props, context) { super(props, context); - this.state = { - zoomScale: 0, - minZoomScale: 0, - maxZoomScale: 0, - panTranslateX: 0, - panTranslateY: 0, - }; - - this.debouncedCacheZoom = debounce(this.cacheZoom.bind(this), ZOOM_CACHE_DEBOUNCE_INTERVAL); this.handleMouseClick = this.handleMouseClick.bind(this); - this.zoomed = this.zoomed.bind(this); } - componentDidMount() { - // distinguish pan/zoom from click - this.isZooming = false; - this.zoomRestored = false; - this.zoom = zoom().on('zoom', this.zoomed); - - this.svg = select('.nodes-chart svg'); - this.svg.call(this.zoom); - - this.restoreCachedZoom(this.props); - } - - componentWillUnmount() { - // undoing .call(zoom) - this.svg - .on('mousedown.zoom', null) - .on('onwheel', null) - .on('onmousewheel', null) - .on('dblclick.zoom', null) - .on('touchstart.zoom', null); - - this.debouncedCacheZoom.cancel(); - } - - componentWillReceiveProps(nextProps) { - const layoutChanged = nextProps.layoutId !== this.props.layoutId; - - // If the layout has changed (either active topology or its options) or - // relayouting has been requested, stop pending zoom caching event and - // ask for the new zoom settings to be restored again from the cache. - if (layoutChanged || nextProps.forceRelayout) { - this.debouncedCacheZoom.cancel(); - this.zoomRestored = false; - } - - if (!this.zoomRestored) { - this.restoreCachedZoom(nextProps); + handleMouseClick() { + if (this.props.selectedNodeId) { + this.props.clickBackground(); } } render() { - // Not passing transform into child components for perf reasons. - const { panTranslateX, panTranslateY, zoomScale } = this.state; - const transform = `translate(${panTranslateX}, ${panTranslateY}) scale(${zoomScale})`; - const svgClassNames = this.props.isEmpty ? 'hide' : ''; - const markerOffset = this.props.selectedNodeId ? '35' : '40'; - const markerSize = this.props.selectedNodeId ? '10' : '30'; - + const { selectedNodeId } = this.props; return (
- - - - - - - - - - + + + + + +
); } - - 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) { - 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(); - } - } } function mapStateToProps(state) { return { - layoutZoom: activeLayoutZoomSelector(state), - layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)), selectedNodeId: state.get('selectedNodeId'), - forceRelayout: state.get('forceRelayout'), }; } export default connect( mapStateToProps, - { clickBackground, cacheZoomState } + { clickBackground } )(NodesChart); diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js index 1512d142d..719c3df2d 100644 --- a/client/app/scripts/charts/nodes-grid.js +++ b/client/app/scripts/charts/nodes-grid.js @@ -7,7 +7,7 @@ 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 { canvasMarginsSelector, canvasHeightSelector } from '../selectors/canvas'; import { searchNodeMatchesSelector } from '../selectors/search'; import { getNodeColor } from '../utils/color-utils'; @@ -97,14 +97,15 @@ 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, }; + // TODO: What are 24 and 18? Use a comment or extract into constants. const tbodyHeight = height - 24 - 18; const className = 'scroll-body'; const tbodyStyle = { @@ -146,6 +147,8 @@ class NodesGrid extends React.Component { 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'), @@ -153,7 +156,6 @@ function mapStateToProps(state) { searchNodeMatches: searchNodeMatchesSelector(state), searchQuery: state.get('searchQuery'), selectedNodeId: state.get('selectedNodeId'), - height: state.getIn(['viewport', 'height']), }; } diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 19d7d9245..de82efc60 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -16,13 +16,18 @@ 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 MetricSelector from './metric-selector'; +import ViewModeSelector from './view-mode-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 { availableNetworksSelector } from '../selectors/node-networks'; +import { + activeTopologyOptionsSelector, + isResourceViewModeSelector, + 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 { gridMode, showingDetails, showingHelp, showingMetricsSelector, + const { isTableViewMode, isGraphViewMode, isResourceViewMode, showingDetails, showingHelp, showingNetworkSelector, showingTroubleshootingMenu } = this.props; const isIframe = window !== window.top; @@ -124,16 +129,15 @@ class App extends React.Component { - + - - {showingMetricsSelector && !gridMode && } - {showingNetworkSelector && !gridMode && } - - + + {showingNetworkSelector && isGraphViewMode && } + {!isResourceViewMode && } + {!isResourceViewMode && }