mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 10:11:03 +00:00
Merge pull request #136 from weaveworks/node-highlighting
highlight connected nodes/edges on hover
This commit is contained in:
@@ -39,6 +39,13 @@ module.exports = {
|
||||
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
|
||||
},
|
||||
|
||||
enterEdge: function(edgeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.ENTER_EDGE,
|
||||
edgeId: edgeId
|
||||
});
|
||||
},
|
||||
|
||||
enterNode: function(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.ENTER_NODE,
|
||||
@@ -53,6 +60,13 @@ module.exports = {
|
||||
RouterUtils.updateRoute();
|
||||
},
|
||||
|
||||
leaveEdge: function(edgeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.LEAVE_EDGE,
|
||||
edgeId: edgeId
|
||||
});
|
||||
},
|
||||
|
||||
leaveNode: function(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.LEAVE_NODE,
|
||||
|
||||
34
client/app/scripts/charts/edge.js
Normal file
34
client/app/scripts/charts/edge.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const d3 = require('d3');
|
||||
const React = require('react');
|
||||
|
||||
const AppActions = require('../actions/app-actions');
|
||||
|
||||
const line = d3.svg.line()
|
||||
.interpolate('basis')
|
||||
.x(function(d) { return d.x; })
|
||||
.y(function(d) { return d.y; });
|
||||
|
||||
const Edge = React.createClass({
|
||||
|
||||
render: function() {
|
||||
const className = this.props.highlighted ? 'edge highlighted' : 'edge';
|
||||
|
||||
return (
|
||||
<g className={className} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} id={this.props.id}>
|
||||
<path d={line(this.props.points)} className="shadow" />
|
||||
<path d={line(this.props.points)} className="link" />
|
||||
</g>
|
||||
);
|
||||
},
|
||||
|
||||
handleMouseEnter: function(ev) {
|
||||
AppActions.enterEdge(ev.currentTarget.id);
|
||||
},
|
||||
|
||||
handleMouseLeave: function(ev) {
|
||||
AppActions.leaveEdge(ev.currentTarget.id);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = Edge;
|
||||
@@ -1,6 +1,7 @@
|
||||
const React = require('react');
|
||||
const tweenState = require('react-tween-state');
|
||||
|
||||
const AppActions = require('../actions/app-actions');
|
||||
const NodeColorMixin = require('../mixins/node-color-mixin');
|
||||
|
||||
const Node = React.createClass({
|
||||
@@ -47,7 +48,9 @@ const Node = React.createClass({
|
||||
const className = this.props.highlighted ? 'node highlighted' : 'node';
|
||||
|
||||
return (
|
||||
<g className={className} transform={transform} onClick={this.props.onClick} id={this.props.id}>
|
||||
<g className={className} transform={transform} id={this.props.id}
|
||||
onClick={this.props.onClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
{this.props.highlighted && <circle r={scale(0.7)} className="highlighted"></circle>}
|
||||
<circle r={scale(0.5)} className="border" stroke={color}></circle>
|
||||
<circle r={scale(0.45)} className="shadow"></circle>
|
||||
<circle r={Math.max(2, scale(0.125))} className="node"></circle>
|
||||
@@ -55,7 +58,16 @@ const Node = React.createClass({
|
||||
<text className="node-sublabel" textAnchor="middle" x={textOffsetX} y={textOffsetY + 17}>{this.props.subLabel}</text>
|
||||
</g>
|
||||
);
|
||||
},
|
||||
|
||||
handleMouseEnter: function(ev) {
|
||||
AppActions.enterNode(ev.currentTarget.id);
|
||||
},
|
||||
|
||||
handleMouseLeave: function(ev) {
|
||||
AppActions.leaveNode(ev.currentTarget.id);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = Node;
|
||||
|
||||
@@ -2,6 +2,8 @@ const _ = require('lodash');
|
||||
const d3 = require('d3');
|
||||
const React = require('react');
|
||||
|
||||
const Edge = require('./edge');
|
||||
const Naming = require('../constants/naming');
|
||||
const NodesLayout = require('./nodes-layout');
|
||||
const Node = require('./node');
|
||||
|
||||
@@ -12,11 +14,6 @@ const MARGINS = {
|
||||
bottom: 0
|
||||
};
|
||||
|
||||
const line = d3.svg.line()
|
||||
.interpolate('basis')
|
||||
.x(function(d) { return d.x; })
|
||||
.y(function(d) { return d.y; });
|
||||
|
||||
const NodesChart = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
@@ -77,9 +74,9 @@ const NodesChart = React.createClass({
|
||||
return fingerprint.join(';');
|
||||
},
|
||||
|
||||
getGraphNodes: function(nodes, scale) {
|
||||
renderGraphNodes: function(nodes, scale) {
|
||||
return _.map(nodes, function(node) {
|
||||
const highlighted = _.includes(this.props.highlightedNodes, node.id);
|
||||
const highlighted = _.includes(this.props.highlightedNodeIds, node.id);
|
||||
return (
|
||||
<Node
|
||||
highlighted={highlighted}
|
||||
@@ -96,17 +93,18 @@ const NodesChart = React.createClass({
|
||||
}, this);
|
||||
},
|
||||
|
||||
getGraphEdges: function(edges) {
|
||||
renderGraphEdges: function(edges) {
|
||||
return _.map(edges, function(edge) {
|
||||
const highlighted = _.includes(this.props.highlightedEdgeIds, edge.id);
|
||||
return (
|
||||
<path className="link" d={line(edge.points)} key={edge.id} />
|
||||
<Edge key={edge.id} id={edge.id} points={edge.points} highlighted={highlighted} />
|
||||
);
|
||||
});
|
||||
}, this);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const nodeElements = this.getGraphNodes(this.state.nodes, this.state.nodeScale);
|
||||
const edgeElements = this.getGraphEdges(this.state.edges, this.state.nodeScale);
|
||||
const nodeElements = this.renderGraphNodes(this.state.nodes, this.state.nodeScale);
|
||||
const edgeElements = this.renderGraphEdges(this.state.edges, this.state.nodeScale);
|
||||
const transform = 'translate(' + this.state.translate + ')' +
|
||||
' scale(' + this.state.scale + ')';
|
||||
|
||||
@@ -131,12 +129,17 @@ const NodesChart = React.createClass({
|
||||
|
||||
_.each(topology, function(node, id) {
|
||||
nodes[id] = prevNodes[id] || {};
|
||||
|
||||
// initialize position for new nodes
|
||||
_.defaults(nodes[id], {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
textAnchor: 'start'
|
||||
});
|
||||
|
||||
// copy relevant fields to state nodes
|
||||
_.assign(nodes[id], {
|
||||
adjacency: node.adjacency,
|
||||
id: id,
|
||||
label: node.label_major,
|
||||
subLabel: node.label_minor,
|
||||
@@ -153,7 +156,7 @@ const NodesChart = React.createClass({
|
||||
_.each(topology, function(node) {
|
||||
_.each(node.adjacency, function(adjacent) {
|
||||
const edge = [node.id, adjacent];
|
||||
const edgeId = edge.join('-');
|
||||
const edgeId = edge.join(Naming.EDGE_ID_SEPARATOR);
|
||||
|
||||
if (!edges[edgeId]) {
|
||||
const source = nodes[edge[0]];
|
||||
|
||||
@@ -19,6 +19,8 @@ function getStateFromStores() {
|
||||
currentTopology: AppStore.getCurrentTopology(),
|
||||
connectionState: AppStore.getConnectionState(),
|
||||
currentGrouping: AppStore.getCurrentGrouping(),
|
||||
highlightedEdgeIds: AppStore.getHighlightedEdgeIds(),
|
||||
highlightedNodeIds: AppStore.getHighlightedNodeIds(),
|
||||
selectedNodeId: AppStore.getSelectedNodeId(),
|
||||
nodeDetails: AppStore.getNodeDetails(),
|
||||
nodes: AppStore.getNodes(),
|
||||
@@ -67,7 +69,8 @@ const App = React.createClass({
|
||||
<Status connectionState={this.state.connectionState} />
|
||||
</div>
|
||||
|
||||
<Nodes nodes={this.state.nodes} />
|
||||
<Nodes nodes={this.state.nodes} highlightedNodeIds={this.state.highlightedNodeIds}
|
||||
highlightedEdgeIds={this.state.highlightedEdgeIds} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,8 +31,10 @@ const Nodes = React.createClass({
|
||||
return (
|
||||
<div id="nodes">
|
||||
<NodesChart
|
||||
onNodeClick={this.onNodeClick}
|
||||
highlightedEdgeIds={this.props.highlightedEdgeIds}
|
||||
highlightedNodeIds={this.props.highlightedNodeIds}
|
||||
nodes={this.props.nodes}
|
||||
onNodeClick={this.onNodeClick}
|
||||
width={this.state.width}
|
||||
height={this.state.height}
|
||||
context="view"
|
||||
|
||||
@@ -5,8 +5,10 @@ module.exports = keymirror({
|
||||
CLICK_GROUPING: null,
|
||||
CLICK_NODE: null,
|
||||
CLICK_TOPOLOGY: null,
|
||||
ENTER_EDGE: null,
|
||||
ENTER_NODE: null,
|
||||
HIT_ESC_KEY: null,
|
||||
LEAVE_EDGE: null,
|
||||
LEAVE_NODE: null,
|
||||
RECEIVE_NODE_DETAILS: null,
|
||||
RECEIVE_NODES: null,
|
||||
|
||||
4
client/app/scripts/constants/naming.js
Normal file
4
client/app/scripts/constants/naming.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
module.exports = {
|
||||
EDGE_ID_SEPARATOR: '-'
|
||||
};
|
||||
@@ -4,6 +4,7 @@ const assign = require('object-assign');
|
||||
|
||||
const AppDispatcher = require('../dispatcher/app-dispatcher');
|
||||
const ActionTypes = require('../constants/action-types');
|
||||
const Naming = require('../constants/naming');
|
||||
|
||||
// Helpers
|
||||
|
||||
@@ -16,7 +17,8 @@ function isUrlForTopologyId(url, topologyId) {
|
||||
let connectionState = 'disconnected';
|
||||
let currentGrouping = 'none';
|
||||
let currentTopologyId = 'applications';
|
||||
let mouseOverNode = null;
|
||||
let mouseOverEdgeId = null;
|
||||
let mouseOverNodeId = null;
|
||||
let nodes = {};
|
||||
let nodeDetails = null;
|
||||
let selectedNodeId = null;
|
||||
@@ -58,6 +60,36 @@ const AppStore = assign({}, EventEmitter.prototype, {
|
||||
return currentGrouping;
|
||||
},
|
||||
|
||||
getHighlightedEdgeIds: function() {
|
||||
if (mouseOverNodeId) {
|
||||
// all neighbour combinations because we dont know which direction exists
|
||||
const node = nodes[mouseOverNodeId];
|
||||
return _.flatten(
|
||||
_.map(node.adjacency, function(nodeId) {
|
||||
return [
|
||||
[nodeId, mouseOverNodeId].join(Naming.EDGE_ID_SEPARATOR),
|
||||
[mouseOverNodeId, nodeId].join(Naming.EDGE_ID_SEPARATOR)
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
if (mouseOverEdgeId) {
|
||||
return mouseOverEdgeId;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getHighlightedNodeIds: function() {
|
||||
if (mouseOverNodeId) {
|
||||
const node = nodes[mouseOverNodeId];
|
||||
return _.union(node.adjacency, [mouseOverNodeId]);
|
||||
}
|
||||
if (mouseOverEdgeId) {
|
||||
return mouseOverEdgeId.split(Naming.EDGE_ID_SEPARATOR);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getNodeDetails: function() {
|
||||
return nodeDetails;
|
||||
},
|
||||
@@ -110,8 +142,13 @@ AppStore.registeredCallback = function(payload) {
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.ENTER_EDGE:
|
||||
mouseOverEdgeId = payload.edgeId;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.ENTER_NODE:
|
||||
mouseOverNode = payload.nodeId;
|
||||
mouseOverNodeId = payload.nodeId;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
@@ -121,8 +158,13 @@ AppStore.registeredCallback = function(payload) {
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.LEAVE_EDGE:
|
||||
mouseOverEdgeId = null;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.LEAVE_NODE:
|
||||
mouseOverNode = null;
|
||||
mouseOverNodeId = null;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
@@ -142,8 +184,11 @@ AppStore.registeredCallback = function(payload) {
|
||||
// nodes that no longer exist
|
||||
_.each(payload.delta.remove, function(nodeId) {
|
||||
// in case node disappears before mouseleave event
|
||||
if (mouseOverNode === nodeId) {
|
||||
mouseOverNode = null;
|
||||
if (mouseOverNodeId === nodeId) {
|
||||
mouseOverNodeId = null;
|
||||
}
|
||||
if (nodes[nodeId] && _.contains(mouseOverEdgeId, nodeId)) {
|
||||
mouseOverEdgeId = null;
|
||||
}
|
||||
delete nodes[nodeId];
|
||||
});
|
||||
|
||||
@@ -162,13 +162,27 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link {
|
||||
stroke: @text-secondary-color;
|
||||
stroke-width: 1.5px;
|
||||
fill: none;
|
||||
opacity: 0.5;
|
||||
.edge {
|
||||
.link {
|
||||
stroke: @text-secondary-color;
|
||||
stroke-width: 1.5px;
|
||||
fill: none;
|
||||
stroke-opacity: 0.5;
|
||||
}
|
||||
.shadow {
|
||||
stroke: @weave-blue;
|
||||
stroke-width: 10px;
|
||||
fill: none;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
&.highlighted {
|
||||
.shadow {
|
||||
stroke-opacity: 0.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
circle.border {
|
||||
stroke-width: 3px;
|
||||
fill: none;
|
||||
@@ -182,6 +196,15 @@ body {
|
||||
circle.node {
|
||||
fill: @text-color;
|
||||
}
|
||||
|
||||
circle.highlighted {
|
||||
fill: @weave-blue;
|
||||
fill-opacity: 0.1;
|
||||
stroke: @weave-blue;
|
||||
stroke-width: 1px;
|
||||
stroke-opacity: 0.4;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#details {
|
||||
|
||||
Reference in New Issue
Block a user