diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 2199797349..fd33701fb4 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 44f71e4227..fb9e1eca4a 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 1512d142d9..719c3df2dc 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 19d7d92454..de82efc600 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 && }