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": {
diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js
index 039ac5f21..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)
@@ -288,25 +291,29 @@ 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) {
+ // normalize graph coordinates by zoomScale
+ const zoomScale = state.scale;
+ const outerRadius = radius + this.state.nodeScale(2);
+ if (offsetX + (centerX + outerRadius) * zoomScale > visibleWidth) {
// shift left if blocked by details
- const shift = centerX + radius - visibleWidth;
+ const shift = (centerX + outerRadius) * zoomScale - visibleWidth;
translate[0] = -shift;
- } else if (offsetX + centerX - radius < 0) {
+ } else if (offsetX + (centerX - outerRadius) * zoomScale < 0) {
// shift right if off canvas
- const shift = offsetX - offsetX + centerX - radius;
+ const shift = offsetX - offsetX + (centerX - outerRadius) * zoomScale;
translate[0] = -shift;
}
const offsetY = translate[1];
- if (offsetY + centerY + radius > props.height) {
+ if (offsetY + (centerY + outerRadius) * zoomScale > props.height) {
// shift up if past bottom
- const shift = centerY + radius - props.height;
+ const shift = (centerY + outerRadius) * zoomScale - props.height;
translate[1] = -shift;
- } else if (offsetY + centerY - radius - props.topMargin < 0) {
+ } else if (offsetY + (centerY - outerRadius) * zoomScale - props.topMargin < 0) {
// shift down if off canvas
- const shift = offsetY - offsetY + centerY - radius - 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);
@@ -318,6 +325,10 @@ const NodesChart = React.createClass({
};
},
+ handleBackgroundClick: function() {
+ AppActions.clickCloseDetails();
+ },
+
restoreLayout: function(state) {
const edges = state.edges;
const nodes = state.nodes;
@@ -348,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(
@@ -398,6 +409,7 @@ const NodesChart = React.createClass({
},
zoomed: function() {
+ // debug('zoomed', d3.event.scale, d3.event.translate);
this.setState({
hasZoomed: true,
panTranslate: d3.event.translate.slice(),
diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js
index b02e79768..9f1fa120b 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() {
@@ -26,6 +30,7 @@ function getStateFromStores() {
nodeDetails: AppStore.getNodeDetails(),
nodes: AppStore.getNodes(),
topologies: AppStore.getTopologies(),
+ topologiesLoaded: AppStore.isTopologiesLoaded(),
version: AppStore.getVersion(),
websocketClosed: AppStore.isWebsocketClosed()
};
@@ -57,11 +62,17 @@ 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 : '';
// width of details panel blocking a view
- const detailsWidth = showingDetails ? 420 : 0;
+ const detailsWidth = showingDetails ? 450 : 0;
const topMargin = 100;
return (
@@ -73,9 +84,6 @@ const App = React.createClass({
-
-
+
+
+
+
+
);
- }
+ },
+ 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..43a698d6b 100644
--- a/client/app/scripts/components/status.js
+++ b/client/app/scripts/components/status.js
@@ -2,22 +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...
-
- );
- }
- },
-
render: function() {
+ 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 (
-
- {this.renderConnectionState(this.props.errorUrl, this.props.websocketClosed)}
+
+ {showWarningIcon && }
+ {text}
);
}
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/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 85d063a05..f7d843f60 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;
@@ -45,11 +35,49 @@ html, body {
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", 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,15 +163,10 @@ body {
}
.status {
- float: right;
- margin-top: 14px;
- margin-right: 64px;
-
&-icon {
font-size: 16px;
position: relative;
top: 1px;
- color: @text-secondary-color;
}
&-label {
@@ -280,26 +303,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 +314,7 @@ body {
&-label {
color: white;
- margin-bottom: 0;
+ margin: 0;
width: 348px;
&-minor {
@@ -330,7 +333,6 @@ body {
position: absolute;
top: 115px;
bottom: 0;
- width: 100%;
padding: 0 36px 0 36px;
overflow-y: scroll;
@@ -383,3 +385,45 @@ body {
}
+.sidebar {
+ position: fixed;
+ bottom: 16px;
+ left: 20px;
+ width: 18em;
+ 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;
+ }
+ &.status-loading {
+ animation: status-loading 2.0s infinite ease-in-out;
+ }
+
+ &-action {
+ float: right;
+ text-transform: uppercase;
+ font-weight: bold;
+ color: darken(@weave-orange, 25%);
+ cursor: pointer;
+ font-size: 90%;
+ }
+ }
+}
+
+@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;
+ }
+}
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",