Commit 2e8df4a8 authored by David's avatar David
Browse files

Merge pull request #136 from weaveworks/node-highlighting

highlight connected nodes/edges on hover
parents dad90e3d d259be72
master 1955-client-perf-regressions 2030-node-linking 2492-state-shape 3810-cordon-control Remove-stale-external-collaborators add-weave-cloud aliyun-sls-master better-nat bia-layout-prototype bump-alpine check-blank-id combined-view-ui consul-cleanup container-netns count-real-hosts cri-container-state deepthought delete-data dependabot/go_modules/github.com/weaveworks/weave-2.6.3incompatible deprecate-host-node-id docker-os dont-stream-children expression-engine feature/read-password-from-file feature/read-password-from-file-faq fewer-closed-connections filter-pseudo-pseudo-adjacencies fix-build fix-process-containername fix-show-in gh-pages go-1-17-8 golangci-lint graph-dsl hier-s3-key latest-map-merge-old-into-young less-quay limit-parallel-decoding lower-sirupsen-clean-approach-with-gvt map-container-to-pod-ip merge-old-into-young mike/middleware/add-headers mike/temp more-renderers-b no-hex-hostnames no-latest-timestamp node-unsafe-merge object-object-url pod-name-promql popup-terminal-delete-pipe prometheus-plugin release-1.11 release-1.7 release-1.8 release-1.9 remove-endpoint-benchmark remove-proxima s390x-support scan-data scope-cortex-singleton scope_s390x sets-sorted-slice shfmt simplify-load-report sliced-maps2 streamline-withparents test-marshall-backwards thingdoer tour-de-scope troubleshoot-consul unsafe-merge-nodes update-aws-sdk update-common-lower-sirupsen update-lodash update-node-deps update-opentracing update-ugorji vendoring wheel-event wip-aws-timestamp wip-child-ids wip-drop-map-image wip-render-docs wip-simplify-connection-join withlatests-slice 1.13.0 v1.13.2 v1.13.1 v1.12.0 v1.11.6 v1.11.5 v1.11.4 v1.11.3 v1.11.2 v1.11.1 v1.11.0 v1.10.2 v1.10.1 v1.10.0 v1.9.1 v1.9.0 v1.8.1 v1.8.0 v1.7.3 v1.7.2 v1.7.1 v1.7.0 v1.6.7 v1.6.6 v1.6.5 v1.6.4 v1.6.3 v1.6.2 v1.6.1 v1.6.0 v1.5.1 v1.5.0 v1.4.0 v1.3.0 v1.2.1 v1.2.0 v1.1.0 v1.0.0 v0.17.1 v0.17.0 v0.16.2 v0.16.1 v0.16.0 v0.15.0 v0.14.0 v0.13.1 v0.13.0 v0.12.0 v0.11.1 v0.11.0 v0.10.0 v0.9.0 v0.8.0 v0.7.0 v0.6.0 v0.5.0 v0.4.0 v0.3.0 v0.2.0 v0.1.0 latest_release lastest_release demo
No related merge requests found
Showing with 168 additions and 26 deletions
+168 -26
......@@ -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,
......
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;
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,
......
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 {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment