mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 02:00:43 +00:00
Merge pull request #487 from weaveworks/sidebar
New sidebar in the bottom left
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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({
|
||||
<div className="header">
|
||||
<Logo />
|
||||
<Topologies topologies={this.state.topologies} currentTopology={this.state.currentTopology} />
|
||||
<TopologyOptions options={this.state.currentTopologyOptions}
|
||||
activeOptions={this.state.activeTopologyOptions} />
|
||||
<Status errorUrl={this.state.errorUrl} websocketClosed={this.state.websocketClosed} />
|
||||
</div>
|
||||
|
||||
<Nodes nodes={this.state.nodes} highlightedNodeIds={this.state.highlightedNodeIds}
|
||||
@@ -83,14 +91,25 @@ const App = React.createClass({
|
||||
selectedNodeId={this.state.selectedNodeId} topMargin={topMargin}
|
||||
topologyId={this.state.currentTopologyId} />
|
||||
|
||||
<Sidebar>
|
||||
<TopologyOptions options={this.state.currentTopologyOptions}
|
||||
activeOptions={this.state.activeTopologyOptions} />
|
||||
<Status errorUrl={this.state.errorUrl} topology={this.state.currentTopology}
|
||||
topologiesLoaded={this.state.topologiesLoaded}
|
||||
websocketClosed={this.state.websocketClosed} />
|
||||
</Sidebar>
|
||||
|
||||
<div className="footer">
|
||||
{versionString}
|
||||
<a href="https://gitreports.com/issue/weaveworks/scope" target="_blank">Report an issue</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
muiTheme: React.PropTypes.object
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = App;
|
||||
|
||||
@@ -10,7 +10,7 @@ const Details = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div id="details">
|
||||
<Paper zDepth={3}>
|
||||
<Paper zDepth={3} style={{height: '100%', paddingBottom: 8}}>
|
||||
<div className="details-tools-wrapper">
|
||||
<div className="details-tools">
|
||||
<span className="fa fa-close" onClick={this.handleClickClose} />
|
||||
|
||||
15
client/app/scripts/components/sidebar.js
Normal file
15
client/app/scripts/components/sidebar.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const React = require('react');
|
||||
|
||||
const Sidebar = React.createClass({
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="sidebar">
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = Sidebar;
|
||||
@@ -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 (
|
||||
<div className="status-connection" title={title}>
|
||||
<span className="status-icon fa fa-exclamation-circle" />
|
||||
<span className="status-label">Trying to reconnect...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
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 (
|
||||
<div className="status">
|
||||
{this.renderConnectionState(this.props.errorUrl, this.props.websocketClosed)}
|
||||
<div className={classNames}>
|
||||
{showWarningIcon && <span className="status-icon fa fa-exclamation-circle" />}
|
||||
<span className="status-label" title={title}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
22
client/app/scripts/components/topology-option-action.js
Normal file
22
client/app/scripts/components/topology-option-action.js
Normal file
@@ -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 (
|
||||
<span className="sidebar-item-action" onClick={this.onClick}>
|
||||
{this.props.value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = TopologyOptionAction;
|
||||
@@ -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 (
|
||||
<TopologyOptionAction option={option} value={action} />
|
||||
);
|
||||
},
|
||||
|
||||
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 (
|
||||
<DropDownMenu menuItems={menuItems} onChange={this.onChange} key={key}
|
||||
selectedIndex={selected} />
|
||||
<div className="sidebar-item">
|
||||
{activeText}
|
||||
<span className="sidebar-item-actions">
|
||||
{actions}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -51,27 +46,14 @@ const TopologyOptions = React.createClass({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="topology-options" ref="container">
|
||||
<div className="topology-options">
|
||||
{options.map(function(items) {
|
||||
return this.renderOption(items);
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user