From 8483492eb787daef44d3b49279189684ca101151 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 14 Sep 2015 20:45:10 +0200 Subject: [PATCH 1/7] New sidebar in the bottom left * status bar (fixes #207) * moved topology options to sidebar * render topology option like snackbar * upgrade material-ui to 0.11 --- client/app/scripts/components/app.js | 25 +++++- client/app/scripts/components/details.js | 2 +- client/app/scripts/components/sidebar.js | 15 ++++ client/app/scripts/components/status.js | 11 ++- .../components/topology-option-action.js | 22 +++++ .../scripts/components/topology-options.js | 58 +++++-------- client/app/styles/main.less | 83 +++++++++++-------- client/package.json | 2 +- 8 files changed, 137 insertions(+), 81 deletions(-) create mode 100644 client/app/scripts/components/sidebar.js create mode 100644 client/app/scripts/components/topology-option-action.js diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index b02e79768..89f7b52d2 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -1,7 +1,9 @@ const React = require('react'); +const mui = require('material-ui'); const Logo = require('./logo'); const AppStore = require('../stores/app-store'); +const Sidebar = require('./sidebar.js'); const Status = require('./status.js'); const Topologies = require('./topologies.js'); const TopologyOptions = require('./topology-options.js'); @@ -11,6 +13,8 @@ const Details = require('./details'); const Nodes = require('./nodes'); const RouterUtils = require('../utils/router-utils'); +const ThemeManager = new mui.Styles.ThemeManager(); + const ESC_KEY_CODE = 27; function getStateFromStores() { @@ -57,6 +61,12 @@ const App = React.createClass({ } }, + getChildContext: function() { + return { + muiTheme: ThemeManager.getCurrentTheme() + }; + }, + render: function() { const showingDetails = this.state.selectedNodeId; const versionString = this.state.version ? 'Version ' + this.state.version : ''; @@ -73,9 +83,6 @@ const App = React.createClass({
- -
+ + + + +
{versionString}   Report an issue
); - } + }, + childContextTypes: { + muiTheme: React.PropTypes.object + } }); module.exports = App; diff --git a/client/app/scripts/components/details.js b/client/app/scripts/components/details.js index bccab8f1a..811b7b642 100644 --- a/client/app/scripts/components/details.js +++ b/client/app/scripts/components/details.js @@ -10,7 +10,7 @@ const Details = React.createClass({ render: function() { return (
- +
diff --git a/client/app/scripts/components/sidebar.js b/client/app/scripts/components/sidebar.js new file mode 100644 index 000000000..659b7226e --- /dev/null +++ b/client/app/scripts/components/sidebar.js @@ -0,0 +1,15 @@ +const React = require('react'); + +const Sidebar = React.createClass({ + + render: function() { + return ( +
+ {this.props.children} +
+ ); + } + +}); + +module.exports = Sidebar; diff --git a/client/app/scripts/components/status.js b/client/app/scripts/components/status.js index d5b726f58..cea0dc382 100644 --- a/client/app/scripts/components/status.js +++ b/client/app/scripts/components/status.js @@ -14,10 +14,17 @@ const Status = React.createClass({ } }, + renderTopologyStats: function(stats) { + const statsText = `${stats.node_count} nodes, ${stats.edge_count} connections`; + return
{statsText}
; + }, + render: function() { + const showStats = this.props.topology && !this.props.errorUrl && !this.props.websocketClosed; return ( -
- {this.renderConnectionState(this.props.errorUrl, this.props.websocketClosed)} +
+ {showStats && this.renderTopologyStats(this.props.topology.stats)} + {!showStats && this.renderConnectionState(this.props.errorUrl, this.props.websocketClosed)}
); } diff --git a/client/app/scripts/components/topology-option-action.js b/client/app/scripts/components/topology-option-action.js new file mode 100644 index 000000000..2101bc307 --- /dev/null +++ b/client/app/scripts/components/topology-option-action.js @@ -0,0 +1,22 @@ +const React = require('react'); + +const AppActions = require('../actions/app-actions'); + +const TopologyOptionAction = React.createClass({ + + onClick: function(ev) { + ev.preventDefault(); + AppActions.changeTopologyOption(this.props.option, this.props.value); + }, + + render: function() { + return ( + + {this.props.value} + + ); + } + +}); + +module.exports = TopologyOptionAction; diff --git a/client/app/scripts/components/topology-options.js b/client/app/scripts/components/topology-options.js index 38855a486..435ad62a7 100644 --- a/client/app/scripts/components/topology-options.js +++ b/client/app/scripts/components/topology-options.js @@ -1,40 +1,35 @@ const React = require('react'); const _ = require('lodash'); -const mui = require('material-ui'); -const DropDownMenu = mui.DropDownMenu; -const AppActions = require('../actions/app-actions'); +const TopologyOptionAction = require('./topology-option-action'); const TopologyOptions = React.createClass({ - componentDidMount: function() { - this.fixWidth(); - }, - - onChange: function(ev, index, item) { - ev.preventDefault(); - AppActions.changeTopologyOption(item.option, item.payload); + renderAction: function(action, option) { + return ( + + ); }, renderOption: function(items) { - let selected = 0; - let key; + let activeText; + const actions = []; const activeOptions = this.props.activeOptions; - const menuItems = items.map(function(item, index) { + items.forEach(function(item) { if (activeOptions[item.option] && activeOptions[item.option] === item.value) { - selected = index; + activeText = item.display; + } else { + actions.push(this.renderAction(item.value, item.option)); } - key = item.option; - return { - option: item.option, - payload: item.value, - text: item.display - }; - }); + }, this); return ( - +
+ {activeText} + + {actions} + +
); }, @@ -51,27 +46,14 @@ const TopologyOptions = React.createClass({ ); return ( -
+
{options.map(function(items) { return this.renderOption(items); }, this)}
); - }, - - componentDidUpdate: function() { - this.fixWidth(); - }, - - fixWidth: function() { - const containerNode = this.refs.container.getDOMNode(); - _.each(containerNode.childNodes, function(child) { - // set drop down width to length of current label - const label = child.getElementsByClassName('mui-menu-label')[0]; - const width = label.getBoundingClientRect().width + 40; - child.style.width = width + 'px'; - }); } + }); module.exports = TopologyOptions; diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 85d063a05..9278f55ab 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -1,6 +1,3 @@ -@import "~material-ui/src/less/scaffolding.less"; -@import "~material-ui/src/less/components.less"; - @font-face { font-family: "Roboto"; src: url("../../node_modules/materialize-css/font/roboto/Roboto-Regular.woff2"), @@ -8,7 +5,6 @@ url("../../node_modules/materialize-css/font/roboto/Roboto-Regular.ttf"); } - .browsehappy { margin: 0.2em 0; background: #ccc; @@ -32,12 +28,6 @@ @text-darker-color: @primary-color; @white: @background-secondary-color; -html, body { -} - -.wrap { -} - /* add this class to truncate text with ellipsis, container needs width */ .truncate { white-space: nowrap; @@ -50,6 +40,25 @@ body { background: linear-gradient(30deg, @background-color 0%, @background-secondary-color 100%); color: @text-color; line-height: 150%; + font-family: Roboto, Helvetica, Arial, sans-serif; + font-size: 13px; +} + +p { + line-height: 20px; + padding-top: 6px; + margin-bottom: 14px; + letter-spacing: 0; + font-weight: 400; + color: @text-color; +} + +h2 { + font-size: 34px; + line-height: 40px; + padding-top: 8px; + margin-bottom: 12px; + font-weight: 400; } #app { @@ -135,10 +144,6 @@ body { } .status { - float: right; - margin-top: 14px; - margin-right: 64px; - &-icon { font-size: 16px; position: relative; @@ -280,26 +285,6 @@ body { } } -.mui-paper, .mui-paper-container { - height: 100%; -} - -.mui-drop-down-menu { - .mui-menu-control { - .mui-menu-label { - font-size: 12px; - } - - .mui-menu-control-underline { - border-top: none; - } - - .mui-menu-control-bg { - background-color: transparent; - } - } -} - .node-details { height: 100%; width: 100%; @@ -311,7 +296,7 @@ body { &-label { color: white; - margin-bottom: 0; + margin: 0; width: 348px; &-minor { @@ -383,3 +368,31 @@ body { } +.sidebar { + position: fixed; + bottom: 16px; + left: 24px; + width: 16em; + font-size: 85%; + + &-item { + background-color: darken(@background-color, 8%); + border-radius: 2px; + padding: 4px 8px; + width: 100%; + margin-top: 2px; + + &.status { + background-color: darken(@background-color, 4%); + color: @text-secondary-color; + } + + &-action { + float: right; + text-transform: uppercase; + font-weight: bold; + color: darken(@weave-orange, 15%); + cursor: pointer; + } + } +} diff --git a/client/package.json b/client/package.json index 0d38001c6..841e743ab 100644 --- a/client/package.json +++ b/client/package.json @@ -15,7 +15,7 @@ "immutable": "^3.7.4", "keymirror": "^0.1.1", "lodash": "~3.9.3", - "material-ui": "~0.7.5", + "material-ui": "^0.11.0", "materialize-css": "^0.96.1", "object-assign": "^2.0.0", "page": "^1.6.3", From dfdf93c4407ef890281b136e341ac5ef4a49c1c0 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 14 Sep 2015 20:53:58 +0200 Subject: [PATCH 2/7] Changed topo option labels to show current state --- app/router.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/router.go b/app/router.go index ccdcadee4..a1c866173 100644 --- a/app/router.go +++ b/app/router.go @@ -128,6 +128,8 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { respondWith(w, http.StatusOK, APIDetails{Version: version}) } +// Topology option labels should tell the current state. The first item must +// be the verb to get to that state var topologyRegistry = map[string]topologyView{ "applications": { human: "Applications", @@ -144,8 +146,8 @@ var topologyRegistry = map[string]topologyView{ parent: "", renderer: render.ContainerWithImageNameRenderer{}, options: optionParams{"system": { - {"show", "Show system containers", false, nop}, - {"hide", "Hide system containers", true, render.FilterSystem}, + {"show", "System containers shown", false, nop}, + {"hide", "System containers hidden", true, render.FilterSystem}, }}, }, "containers-by-image": { @@ -153,8 +155,8 @@ var topologyRegistry = map[string]topologyView{ parent: "containers", renderer: render.ContainerImageRenderer, options: optionParams{"system": { - {"show", "Show system containers", false, nop}, - {"hide", "Hide system containers", true, render.FilterSystem}, + {"show", "System containers shown", false, nop}, + {"hide", "System containers hidden", true, render.FilterSystem}, }}, }, "hosts": { From 3a42ec48ea8f4e6f3190fcdbeff50a37a9f062eb Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 15 Sep 2015 15:28:48 +0200 Subject: [PATCH 3/7] style tweaks for sidebar --- client/app/styles/main.less | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 9278f55ab..84078d9d2 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -35,12 +35,31 @@ text-overflow: ellipsis; } +* { + box-sizing: border-box; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +*:before, +*:after { + box-sizing: border-box; +} + +html { + -webkit-font-smoothing: antialiased; +} + +html, +body { + height: 100%; + width: 100%; +} + /* Space out content a bit */ body { background: linear-gradient(30deg, @background-color 0%, @background-secondary-color 100%); color: @text-color; line-height: 150%; - font-family: Roboto, Helvetica, Arial, sans-serif; + font-family: "Roboto", sans-serif; font-size: 13px; } @@ -315,7 +334,6 @@ h2 { position: absolute; top: 115px; bottom: 0; - width: 100%; padding: 0 36px 0 36px; overflow-y: scroll; @@ -372,7 +390,7 @@ h2 { position: fixed; bottom: 16px; left: 24px; - width: 16em; + width: 18em; font-size: 85%; &-item { @@ -391,8 +409,9 @@ h2 { float: right; text-transform: uppercase; font-weight: bold; - color: darken(@weave-orange, 15%); + color: darken(@weave-orange, 25%); cursor: pointer; + font-size: 90%; } } } From 38fa82dee46974ff6b28d665e5012509d07de640 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 15 Sep 2015 16:38:31 +0200 Subject: [PATCH 4/7] animate status bar, add Loading text --- client/app/scripts/components/app.js | 2 ++ client/app/scripts/components/status.js | 47 ++++++++++++++----------- client/app/scripts/stores/app-store.js | 6 ++++ client/app/styles/main.less | 16 +++++++-- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 89f7b52d2..8f23c3b30 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -30,6 +30,7 @@ function getStateFromStores() { nodeDetails: AppStore.getNodeDetails(), nodes: AppStore.getNodes(), topologies: AppStore.getTopologies(), + topologiesLoaded: AppStore.isTopologiesLoaded(), version: AppStore.getVersion(), websocketClosed: AppStore.isWebsocketClosed() }; @@ -94,6 +95,7 @@ const App = React.createClass({ diff --git a/client/app/scripts/components/status.js b/client/app/scripts/components/status.js index cea0dc382..43a698d6b 100644 --- a/client/app/scripts/components/status.js +++ b/client/app/scripts/components/status.js @@ -2,29 +2,34 @@ const React = require('react'); const Status = React.createClass({ - renderConnectionState: function(errorUrl, websocketClosed) { - if (errorUrl || websocketClosed) { - const title = errorUrl ? 'Cannot reach Scope. Make sure the following URL is reachable: ' + errorUrl : ''; - return ( -
- - Trying to reconnect... -
- ); - } - }, - - renderTopologyStats: function(stats) { - const statsText = `${stats.node_count} nodes, ${stats.edge_count} connections`; - return
{statsText}
; - }, - render: function() { - const showStats = this.props.topology && !this.props.errorUrl && !this.props.websocketClosed; + let title = ''; + let text = 'Trying to reconnect...'; + let showWarningIcon = false; + let classNames = 'status sidebar-item'; + + if (this.props.errorUrl) { + title = `Cannot reach Scope. Make sure the following URL is reachable: ${this.props.errorUrl}`; + classNames += ' status-loading'; + showWarningIcon = true; + } else if (!this.props.topologiesLoaded) { + text = 'Loading topologies...'; + classNames += ' status-loading'; + showWarningIcon = false; + } else if (this.props.websocketClosed) { + classNames += ' status-loading'; + showWarningIcon = true; + } else if (this.props.topology) { + const stats = this.props.topology.stats; + text = `${stats.node_count} nodes, ${stats.edge_count} connections`; + classNames += ' status-stats'; + showWarningIcon = false; + } + return ( -
- {showStats && this.renderTopologyStats(this.props.topology.stats)} - {!showStats && this.renderConnectionState(this.props.errorUrl, this.props.websocketClosed)} +
+ {showWarningIcon && } + {text}
); } diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 09aa0aac4..aa689c7cd 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -56,6 +56,7 @@ let nodes = makeOrderedMap(); let nodeDetails = null; let selectedNodeId = null; let topologies = []; +let topologiesLoaded = false; let websocketClosed = true; function setTopology(topologyId) { @@ -192,6 +193,10 @@ const AppStore = assign({}, EventEmitter.prototype, { return version; }, + isTopologiesLoaded: function() { + return topologiesLoaded; + }, + isWebsocketClosed: function() { return websocketClosed; } @@ -316,6 +321,7 @@ AppStore.registeredCallback = function(payload) { case ActionTypes.RECEIVE_TOPOLOGIES: errorUrl = null; + topologiesLoaded = true; topologies = payload.topologies; if (!currentTopology) { setTopology(currentTopologyId); diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 84078d9d2..f7d843f60 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -167,7 +167,6 @@ h2 { font-size: 16px; position: relative; top: 1px; - color: @text-secondary-color; } &-label { @@ -389,7 +388,7 @@ h2 { .sidebar { position: fixed; bottom: 16px; - left: 24px; + left: 20px; width: 18em; font-size: 85%; @@ -404,6 +403,9 @@ h2 { background-color: darken(@background-color, 4%); color: @text-secondary-color; } + &.status-loading { + animation: status-loading 2.0s infinite ease-in-out; + } &-action { float: right; @@ -415,3 +417,13 @@ h2 { } } } + +@keyframes status-loading { + 0%, 100% { + background-color: darken(@background-color, 4%); + color: @text-secondary-color; + } 50% { + background-color: darken(@background-color, 8%); + color: @text-color; + } +} From 7b9e1e286727a35b5a8d7eb3d2593e7a3c94d421 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 16 Sep 2015 11:30:35 +0200 Subject: [PATCH 5/7] add node radius to circle radius for canvas shift --- client/app/scripts/charts/nodes-chart.js | 17 +++++++++-------- client/app/scripts/components/app.js | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 039ac5f21..738c9f111 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -288,23 +288,24 @@ const NodesChart = React.createClass({ const visibleWidth = Math.max(props.width - props.detailsWidth, 0); const translate = state.translate; const offsetX = translate[0]; - if (offsetX + centerX + radius > visibleWidth) { + const outerRadius = radius + this.state.nodeScale(2); + if (offsetX + centerX + outerRadius > visibleWidth) { // shift left if blocked by details - const shift = centerX + radius - visibleWidth; + const shift = centerX + outerRadius - visibleWidth; translate[0] = -shift; - } else if (offsetX + centerX - radius < 0) { + } else if (offsetX + centerX - outerRadius < 0) { // shift right if off canvas - const shift = offsetX - offsetX + centerX - radius; + const shift = offsetX - offsetX + centerX - outerRadius; translate[0] = -shift; } const offsetY = translate[1]; - if (offsetY + centerY + radius > props.height) { + if (offsetY + centerY + outerRadius > props.height) { // shift up if past bottom - const shift = centerY + radius - props.height; + const shift = centerY + outerRadius - props.height; translate[1] = -shift; - } else if (offsetY + centerY - radius - props.topMargin < 0) { + } else if (offsetY + centerY - outerRadius - props.topMargin < 0) { // shift down if off canvas - const shift = offsetY - offsetY + centerY - radius - props.topMargin; + const shift = offsetY - offsetY + centerY - outerRadius - props.topMargin; translate[1] = -shift; } diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 8f23c3b30..9f1fa120b 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -72,7 +72,7 @@ const App = React.createClass({ const showingDetails = this.state.selectedNodeId; const versionString = this.state.version ? 'Version ' + this.state.version : ''; // width of details panel blocking a view - const detailsWidth = showingDetails ? 420 : 0; + const detailsWidth = showingDetails ? 450 : 0; const topMargin = 100; return ( From 05e21a9d42e8e9079a729184e2a843cbbabbfe08 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 16 Sep 2015 11:48:09 +0200 Subject: [PATCH 6/7] take zoom scale into account when shifting canvas --- client/app/scripts/charts/nodes-chart.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 738c9f111..30d1b9788 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -288,26 +288,29 @@ const NodesChart = React.createClass({ const visibleWidth = Math.max(props.width - props.detailsWidth, 0); const translate = state.translate; const offsetX = translate[0]; + // normalize graph coordinates by zoomScale + const zoomScale = state.scale; const outerRadius = radius + this.state.nodeScale(2); - if (offsetX + centerX + outerRadius > visibleWidth) { + if (offsetX + (centerX + outerRadius) * zoomScale > visibleWidth) { // shift left if blocked by details - const shift = centerX + outerRadius - visibleWidth; + const shift = (centerX + outerRadius) * zoomScale - visibleWidth; translate[0] = -shift; - } else if (offsetX + centerX - outerRadius < 0) { + } else if (offsetX + (centerX - outerRadius) * zoomScale < 0) { // shift right if off canvas - const shift = offsetX - offsetX + centerX - outerRadius; + const shift = offsetX - offsetX + (centerX - outerRadius) * zoomScale; translate[0] = -shift; } const offsetY = translate[1]; - if (offsetY + centerY + outerRadius > props.height) { + if (offsetY + (centerY + outerRadius) * zoomScale > props.height) { // shift up if past bottom - const shift = centerY + outerRadius - props.height; + const shift = (centerY + outerRadius) * zoomScale - props.height; translate[1] = -shift; - } else if (offsetY + centerY - outerRadius - props.topMargin < 0) { + } else if (offsetY + (centerY - outerRadius) * zoomScale - props.topMargin < 0) { // shift down if off canvas - const shift = offsetY - offsetY + centerY - outerRadius - props.topMargin; + const shift = offsetY - offsetY + (centerY - outerRadius) * zoomScale - props.topMargin; translate[1] = -shift; } + // debug('shift', centerX, centerY, outerRadius, translate); // saving translate in d3's panning cache this.zoom.translate(translate); @@ -399,6 +402,7 @@ const NodesChart = React.createClass({ }, zoomed: function() { + // debug('zoomed', d3.event.scale, d3.event.translate); this.setState({ hasZoomed: true, panTranslate: d3.event.translate.slice(), From c1523863c46c42d3377b006725b2b5d6b2d92372 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 16 Sep 2015 12:12:51 +0200 Subject: [PATCH 7/7] Close details on canvas click --- client/app/scripts/charts/nodes-chart.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 30d1b9788..318b29fbe 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -5,6 +5,7 @@ const React = require('react'); const timely = require('timely'); const Spring = require('react-motion').Spring; +const AppActions = require('../actions/app-actions'); const AppStore = require('../stores/app-store'); const Edge = require('./edge'); const Naming = require('../constants/naming'); @@ -28,7 +29,7 @@ const NodesChart = React.createClass({ return { nodes: {}, edges: {}, - nodeScale: 1, + nodeScale: d3.scale.linear(), translate: [0, 0], panTranslate: [0, 0], scale: 1, @@ -47,6 +48,7 @@ const NodesChart = React.createClass({ .on('zoom', this.zoomed); d3.select('.nodes-chart') + .on('click', this.handleBackgroundClick) .call(this.zoom); }, @@ -79,6 +81,7 @@ const NodesChart = React.createClass({ // undoing .call(zoom) d3.select('.nodes-chart') + .on('click', null) .on('mousedown.zoom', null) .on('onwheel', null) .on('onmousewheel', null) @@ -322,6 +325,10 @@ const NodesChart = React.createClass({ }; }, + handleBackgroundClick: function() { + AppActions.clickCloseDetails(); + }, + restoreLayout: function(state) { const edges = state.edges; const nodes = state.nodes; @@ -352,7 +359,7 @@ const NodesChart = React.createClass({ const expanse = Math.min(props.height, props.width); const nodeSize = expanse / 2; - const nodeScale = d3.scale.linear().range([0, nodeSize / Math.pow(n, 0.7)]); + const nodeScale = this.state.nodeScale.range([0, nodeSize / Math.pow(n, 0.7)]); const timedLayouter = timely(NodesLayout.doLayout); const graph = timedLayouter(