mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 01:31:17 +00:00
Merge pull request #473 from weaveworks/324-highlight-selected
Highlight selected node
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
const _ = require('lodash');
|
||||
const d3 = require('d3');
|
||||
const React = require('react');
|
||||
const Spring = require('react-motion').Spring;
|
||||
|
||||
const AppActions = require('../actions/app-actions');
|
||||
|
||||
@@ -8,19 +10,89 @@ const line = d3.svg.line()
|
||||
.x(function(d) { return d.x; })
|
||||
.y(function(d) { return d.y; });
|
||||
|
||||
const animConfig = [80, 20]; // stiffness, bounce
|
||||
|
||||
const flattenPoints = function(points) {
|
||||
const flattened = {};
|
||||
points.forEach(function(point, i) {
|
||||
flattened['x' + i] = {val: point.x, config: animConfig};
|
||||
flattened['y' + i] = {val: point.y, config: animConfig};
|
||||
});
|
||||
return flattened;
|
||||
};
|
||||
|
||||
const extractPoints = function(points) {
|
||||
const extracted = [];
|
||||
_.each(points, function(value, key) {
|
||||
const axis = key[0];
|
||||
const index = key.slice(1);
|
||||
if (!extracted[index]) {
|
||||
extracted[index] = {};
|
||||
}
|
||||
extracted[index][axis] = value.val;
|
||||
});
|
||||
return extracted;
|
||||
};
|
||||
|
||||
const Edge = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
points: []
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.ensureSameLength(this.props.points);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.ensureSameLength(nextProps.points);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const className = this.props.highlighted ? 'edge highlighted' : 'edge';
|
||||
const classNames = ['edge'];
|
||||
const points = flattenPoints(this.props.points);
|
||||
const props = this.props;
|
||||
const handleMouseEnter = this.handleMouseEnter;
|
||||
const handleMouseLeave = this.handleMouseLeave;
|
||||
|
||||
if (this.props.highlighted) {
|
||||
classNames.push('highlighted');
|
||||
}
|
||||
if (this.props.blurred) {
|
||||
classNames.push('blurred');
|
||||
}
|
||||
const classes = classNames.join(' ');
|
||||
|
||||
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>
|
||||
<Spring endValue={points}>
|
||||
{function(interpolated) {
|
||||
const path = line(extractPoints(interpolated));
|
||||
return (
|
||||
<g className={classes} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} id={props.id}>
|
||||
<path d={path} className="shadow" />
|
||||
<path d={path} className="link" />
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
</Spring>
|
||||
);
|
||||
},
|
||||
|
||||
ensureSameLength: function(points) {
|
||||
// Spring needs constant list length, hoping that dagre will insert never more than 10
|
||||
const length = 10;
|
||||
let missing = length - points.length;
|
||||
|
||||
while (missing) {
|
||||
points.unshift(points[0]);
|
||||
missing = length - points.length;
|
||||
}
|
||||
|
||||
return points;
|
||||
},
|
||||
|
||||
handleMouseEnter: function(ev) {
|
||||
AppActions.enterEdge(ev.currentTarget.id);
|
||||
},
|
||||
|
||||
@@ -1,46 +1,16 @@
|
||||
const React = require('react');
|
||||
const tweenState = require('react-tween-state');
|
||||
const Spring = require('react-motion').Spring;
|
||||
|
||||
const AppActions = require('../actions/app-actions');
|
||||
const NodeColorMixin = require('../mixins/node-color-mixin');
|
||||
|
||||
const Node = React.createClass({
|
||||
mixins: [
|
||||
NodeColorMixin,
|
||||
tweenState.Mixin
|
||||
NodeColorMixin
|
||||
],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// initial node position when rendered the first time
|
||||
this.setState({
|
||||
x: this.props.dx,
|
||||
y: this.props.dy
|
||||
});
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// animate node transition to next position
|
||||
this.tweenState('x', {
|
||||
easing: tweenState.easingTypes.easeInOutQuad,
|
||||
duration: 500,
|
||||
endValue: nextProps.dx
|
||||
});
|
||||
this.tweenState('y', {
|
||||
easing: tweenState.easingTypes.easeInOutQuad,
|
||||
duration: 500,
|
||||
endValue: nextProps.dy
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const transform = 'translate(' + this.getTweeningValue('x') + ',' + this.getTweeningValue('y') + ')';
|
||||
const props = this.props;
|
||||
const scale = this.props.scale;
|
||||
const textOffsetX = 0;
|
||||
const textOffsetY = scale(0.5) + 18;
|
||||
@@ -50,29 +20,41 @@ const Node = React.createClass({
|
||||
const onMouseEnter = this.handleMouseEnter;
|
||||
const onMouseLeave = this.handleMouseLeave;
|
||||
const classNames = ['node'];
|
||||
const ellipsis = this.ellipsis;
|
||||
const animConfig = [80, 20]; // stiffness, bounce
|
||||
|
||||
if (this.props.highlighted) {
|
||||
classNames.push('highlighted');
|
||||
}
|
||||
|
||||
if (this.props.blurred) {
|
||||
classNames.push('blurred');
|
||||
}
|
||||
if (this.props.pseudo) {
|
||||
classNames.push('pseudo');
|
||||
}
|
||||
const classes = classNames.join(' ');
|
||||
|
||||
return (
|
||||
<g className={classNames.join(' ')} transform={transform} id={this.props.id}
|
||||
onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
{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>
|
||||
<text className="node-label" textAnchor="middle" x={textOffsetX} y={textOffsetY}>
|
||||
{this.ellipsis(this.props.label, 14)}
|
||||
</text>
|
||||
<text className="node-sublabel" textAnchor="middle" x={textOffsetX} y={textOffsetY + 17}>
|
||||
{this.ellipsis(this.props.subLabel, 12)}
|
||||
</text>
|
||||
</g>
|
||||
<Spring endValue={{x: {val: this.props.dx, config: animConfig}, y: {val: this.props.dy, config: animConfig}}}>
|
||||
{function(interpolated) {
|
||||
const transform = 'translate(' + interpolated.x.val + ',' + interpolated.y.val + ')';
|
||||
return (
|
||||
<g className={classes} transform={transform} id={props.id}
|
||||
onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
{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>
|
||||
<text className="node-label" textAnchor="middle" x={textOffsetX} y={textOffsetY}>
|
||||
{ellipsis(props.label, 14)}
|
||||
</text>
|
||||
<text className="node-sublabel" textAnchor="middle" x={textOffsetX} y={textOffsetY + 17}>
|
||||
{ellipsis(props.subLabel, 12)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
</Spring>
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ const d3 = require('d3');
|
||||
const debug = require('debug')('scope:nodes-chart');
|
||||
const React = require('react');
|
||||
const timely = require('timely');
|
||||
const Spring = require('react-motion').Spring;
|
||||
|
||||
const AppStore = require('../stores/app-store');
|
||||
const Edge = require('./edge');
|
||||
const Naming = require('../constants/naming');
|
||||
const NodesLayout = require('./nodes-layout');
|
||||
@@ -16,6 +18,10 @@ const MARGINS = {
|
||||
bottom: 0
|
||||
};
|
||||
|
||||
// make sure circular layouts lots of nodes spread out
|
||||
const radiusDensity = d3.scale.sqrt()
|
||||
.domain([12, 2]).range([2.5, 5]).clamp(true);
|
||||
|
||||
const NodesChart = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
@@ -23,14 +29,16 @@ const NodesChart = React.createClass({
|
||||
nodes: {},
|
||||
edges: {},
|
||||
nodeScale: 1,
|
||||
translate: '0,0',
|
||||
translate: [0, 0],
|
||||
panTranslate: [0, 0],
|
||||
scale: 1,
|
||||
hasZoomed: false
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.updateGraphState(this.props);
|
||||
const state = this.updateGraphState(this.props, this.state);
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
@@ -43,13 +51,28 @@ const NodesChart = React.createClass({
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.nodes !== this.props.nodes) {
|
||||
this.setState({
|
||||
// gather state, setState should be called only once here
|
||||
const state = _.assign({}, this.state);
|
||||
|
||||
// wipe node states when showing different topology
|
||||
if (nextProps.topologyId !== this.props.topologyId) {
|
||||
_.assign(state, {
|
||||
nodes: {},
|
||||
edges: {}
|
||||
});
|
||||
this.updateGraphState(nextProps);
|
||||
}
|
||||
// FIXME add PureRenderMixin, Immutables, and move the following functions to render()
|
||||
if (nextProps.nodes !== this.props.nodes) {
|
||||
_.assign(state, this.updateGraphState(nextProps, state));
|
||||
}
|
||||
if (this.props.selectedNodeId !== nextProps.selectedNodeId) {
|
||||
_.assign(state, this.restoreLayout(state));
|
||||
}
|
||||
if (nextProps.selectedNodeId) {
|
||||
this.centerSelectedNode(nextProps, state);
|
||||
}
|
||||
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
@@ -76,10 +99,18 @@ const NodesChart = React.createClass({
|
||||
},
|
||||
|
||||
renderGraphNodes: function(nodes, scale) {
|
||||
const hasSelectedNode = this.props.selectedNodeId && this.props.nodes.has(this.props.selectedNodeId);
|
||||
const adjacency = hasSelectedNode ? AppStore.getAdjacentNodes(this.props.selectedNodeId) : null;
|
||||
return _.map(nodes, function(node) {
|
||||
const highlighted = _.includes(this.props.highlightedNodeIds, node.id);
|
||||
const highlighted = _.includes(this.props.highlightedNodeIds, node.id)
|
||||
|| this.props.selectedNodeId === node.id;
|
||||
const blurred = hasSelectedNode
|
||||
&& this.props.selectedNodeId !== node.id
|
||||
&& !adjacency.includes(node.id);
|
||||
|
||||
return (
|
||||
<Node
|
||||
blurred={blurred}
|
||||
highlighted={highlighted}
|
||||
onClick={this.props.onNodeClick}
|
||||
key={node.id}
|
||||
@@ -97,10 +128,17 @@ const NodesChart = React.createClass({
|
||||
},
|
||||
|
||||
renderGraphEdges: function(edges) {
|
||||
const selectedNodeId = this.props.selectedNodeId;
|
||||
const hasSelectedNode = selectedNodeId && this.props.nodes.has(selectedNodeId);
|
||||
|
||||
return _.map(edges, function(edge) {
|
||||
const highlighted = _.includes(this.props.highlightedEdgeIds, edge.id);
|
||||
const blurred = hasSelectedNode
|
||||
&& edge.source.id !== selectedNodeId
|
||||
&& edge.target.id !== selectedNodeId;
|
||||
return (
|
||||
<Edge key={edge.id} id={edge.id} points={edge.points} highlighted={highlighted} />
|
||||
<Edge key={edge.id} id={edge.id} points={edge.points} blurred={blurred}
|
||||
highlighted={highlighted} />
|
||||
);
|
||||
}, this);
|
||||
},
|
||||
@@ -108,30 +146,48 @@ const NodesChart = React.createClass({
|
||||
render: function() {
|
||||
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 + ')';
|
||||
const scale = this.state.scale;
|
||||
|
||||
// only animate shift behavior, not panning
|
||||
const panTranslate = this.state.panTranslate;
|
||||
const shiftTranslate = this.state.translate;
|
||||
let translate = panTranslate;
|
||||
let wasShifted = false;
|
||||
if (shiftTranslate[0] !== panTranslate[0] || shiftTranslate[1] !== panTranslate[1]) {
|
||||
translate = shiftTranslate;
|
||||
wasShifted = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<svg width="100%" height="100%" className="nodes-chart">
|
||||
<g className="canvas" transform={transform}>
|
||||
<g className="edges">
|
||||
{edgeElements}
|
||||
</g>
|
||||
<g className="nodes">
|
||||
{nodeElements}
|
||||
</g>
|
||||
</g>
|
||||
<Spring endValue={{val: translate, config: [80, 20]}}>
|
||||
{function(interpolated) {
|
||||
let interpolatedTranslate = wasShifted ? interpolated.val : panTranslate;
|
||||
const transform = 'translate(' + interpolatedTranslate + ')' +
|
||||
' scale(' + scale + ')';
|
||||
return (
|
||||
<g className="canvas" transform={transform}>
|
||||
<g className="edges">
|
||||
{edgeElements}
|
||||
</g>
|
||||
<g className="nodes">
|
||||
{nodeElements}
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
</Spring>
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
|
||||
initNodes: function(topology, prevNodes) {
|
||||
initNodes: function(topology) {
|
||||
const centerX = this.props.width / 2;
|
||||
const centerY = this.props.height / 2;
|
||||
const nodes = {};
|
||||
|
||||
topology.forEach(function(node, id) {
|
||||
nodes[id] = prevNodes[id] || {};
|
||||
nodes[id] = {};
|
||||
|
||||
// use cached positions if available
|
||||
_.defaults(nodes[id], {
|
||||
@@ -184,14 +240,110 @@ const NodesChart = React.createClass({
|
||||
return edges;
|
||||
},
|
||||
|
||||
updateGraphState: function(props) {
|
||||
centerSelectedNode: function(props, state) {
|
||||
const layoutNodes = state.nodes;
|
||||
const layoutEdges = state.edges;
|
||||
const selectedLayoutNode = layoutNodes[props.selectedNodeId];
|
||||
|
||||
if (!selectedLayoutNode) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const adjacency = AppStore.getAdjacentNodes(props.selectedNodeId);
|
||||
const adjacentLayoutNodes = [];
|
||||
|
||||
adjacency.forEach(function(adjacentId) {
|
||||
adjacentLayoutNodes.push(layoutNodes[adjacentId]);
|
||||
});
|
||||
|
||||
// circle layout for adjacent nodes
|
||||
|
||||
const centerX = selectedLayoutNode.x;
|
||||
const centerY = selectedLayoutNode.y;
|
||||
const adjacentCount = adjacentLayoutNodes.length;
|
||||
const density = radiusDensity(adjacentCount);
|
||||
const radius = Math.min(props.width, props.height) / density;
|
||||
|
||||
_.each(adjacentLayoutNodes, function(node, i) {
|
||||
const angle = Math.PI * 2 * i / adjacentCount;
|
||||
node.x = centerX + radius * Math.sin(angle);
|
||||
node.y = centerY + radius * Math.cos(angle);
|
||||
});
|
||||
|
||||
// fix all edges for circular nodes
|
||||
|
||||
_.each(layoutEdges, function(edge) {
|
||||
if (edge.source === selectedLayoutNode
|
||||
|| edge.target === selectedLayoutNode
|
||||
|| _.includes(adjacentLayoutNodes, edge.source)
|
||||
|| _.includes(adjacentLayoutNodes, edge.target)) {
|
||||
edge.points = [
|
||||
{x: edge.source.x, y: edge.source.y},
|
||||
{x: edge.target.x, y: edge.target.y}
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
// shift canvas selected node out of view
|
||||
const visibleWidth = Math.max(props.width - props.detailsWidth, 0);
|
||||
const translate = state.translate;
|
||||
const offsetX = translate[0];
|
||||
if (offsetX + centerX + radius > visibleWidth) {
|
||||
// shift left if blocked by details
|
||||
const shift = centerX + radius - visibleWidth;
|
||||
translate[0] = -shift;
|
||||
} else if (offsetX + centerX - radius < 0) {
|
||||
// shift right if off canvas
|
||||
const shift = offsetX - offsetX + centerX - radius;
|
||||
translate[0] = -shift;
|
||||
}
|
||||
const offsetY = translate[1];
|
||||
if (offsetY + centerY + radius > props.height) {
|
||||
// shift up if past bottom
|
||||
const shift = centerY + radius - props.height;
|
||||
translate[1] = -shift;
|
||||
} else if (offsetY + centerY - radius - props.topMargin < 0) {
|
||||
// shift down if off canvas
|
||||
const shift = offsetY - offsetY + centerY - radius - props.topMargin;
|
||||
translate[1] = -shift;
|
||||
}
|
||||
|
||||
// saving translate in d3's panning cache
|
||||
this.zoom.translate(translate);
|
||||
|
||||
return {
|
||||
edges: layoutEdges,
|
||||
nodes: layoutNodes,
|
||||
translate: translate
|
||||
};
|
||||
},
|
||||
|
||||
restoreLayout: function(state) {
|
||||
const edges = state.edges;
|
||||
const nodes = state.nodes;
|
||||
|
||||
_.each(nodes, function(node) {
|
||||
node.x = node.px;
|
||||
node.y = node.py;
|
||||
});
|
||||
|
||||
_.each(edges, function(edge) {
|
||||
if (edge.ppoints) {
|
||||
edge.points = edge.ppoints;
|
||||
}
|
||||
});
|
||||
|
||||
return {edges: edges, nodes: nodes};
|
||||
},
|
||||
|
||||
updateGraphState: function(props, state) {
|
||||
const n = props.nodes.size;
|
||||
|
||||
if (n === 0) {
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
const nodes = this.initNodes(props.nodes, this.state.nodes);
|
||||
const nodes = this.initNodes(props.nodes, state.nodes);
|
||||
const edges = this.initEdges(props.nodes, nodes);
|
||||
|
||||
const expanse = Math.min(props.height, props.width);
|
||||
@@ -213,9 +365,18 @@ const NodesChart = React.createClass({
|
||||
|
||||
// layout was aborted
|
||||
if (!graph) {
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
// save coordinates for restore
|
||||
_.each(nodes, function(node) {
|
||||
node.px = node.x;
|
||||
node.py = node.y;
|
||||
});
|
||||
_.each(edges, function(edge) {
|
||||
edge.ppoints = edge.points;
|
||||
});
|
||||
|
||||
// adjust layout based on viewport
|
||||
const xFactor = (props.width - MARGINS.left - MARGINS.right) / graph.width;
|
||||
const yFactor = props.height / graph.height;
|
||||
@@ -228,18 +389,19 @@ const NodesChart = React.createClass({
|
||||
this.zoom.scale(zoomFactor);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
return {
|
||||
nodes: nodes,
|
||||
edges: edges,
|
||||
nodeScale: nodeScale,
|
||||
scale: zoomScale
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
zoomed: function() {
|
||||
this.setState({
|
||||
hasZoomed: true,
|
||||
translate: d3.event.translate,
|
||||
panTranslate: d3.event.translate.slice(),
|
||||
translate: d3.event.translate.slice(),
|
||||
scale: d3.event.scale
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ const App = React.createClass({
|
||||
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 topMargin = 100;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -76,7 +79,8 @@ const App = React.createClass({
|
||||
</div>
|
||||
|
||||
<Nodes nodes={this.state.nodes} highlightedNodeIds={this.state.highlightedNodeIds}
|
||||
highlightedEdgeIds={this.state.highlightedEdgeIds}
|
||||
highlightedEdgeIds={this.state.highlightedEdgeIds} detailsWidth={detailsWidth}
|
||||
selectedNodeId={this.state.selectedNodeId} topMargin={topMargin}
|
||||
topologyId={this.state.currentTopologyId} />
|
||||
|
||||
<div className="footer">
|
||||
|
||||
@@ -33,12 +33,14 @@ const Nodes = React.createClass({
|
||||
<NodesChart
|
||||
highlightedEdgeIds={this.props.highlightedEdgeIds}
|
||||
highlightedNodeIds={this.props.highlightedNodeIds}
|
||||
selectedNodeId={this.props.selectedNodeId}
|
||||
nodes={this.props.nodes}
|
||||
onNodeClick={this.onNodeClick}
|
||||
width={this.state.width}
|
||||
height={this.state.height}
|
||||
topologyId={this.props.topologyId}
|
||||
context="view"
|
||||
detailsWidth={this.props.detailsWidth}
|
||||
topMargin={this.props.topMargin}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -285,5 +285,19 @@ describe('AppStore', function() {
|
||||
expect(AppStore.getNodes().toJS()).toEqual({});
|
||||
});
|
||||
|
||||
// adjacency test
|
||||
|
||||
it('returns the correct adjacency set for a node', function() {
|
||||
registeredCallback(ReceiveNodesDeltaAction);
|
||||
expect(AppStore.getAdjacentNodes().size).toEqual(0);
|
||||
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getAdjacentNodes('n1').size).toEqual(2);
|
||||
expect(AppStore.getAdjacentNodes('n1').has('n1')).toBeTruthy();
|
||||
expect(AppStore.getAdjacentNodes('n1').has('n2')).toBeTruthy();
|
||||
|
||||
registeredCallback(HitEscAction)
|
||||
expect(AppStore.getAdjacentNodes().size).toEqual(0);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -9,6 +9,7 @@ const ActionTypes = require('../constants/action-types');
|
||||
const Naming = require('../constants/naming');
|
||||
|
||||
const makeOrderedMap = Immutable.OrderedMap;
|
||||
const makeSet = Immutable.Set;
|
||||
|
||||
// Helpers
|
||||
|
||||
@@ -44,6 +45,7 @@ function makeNode(node) {
|
||||
// Initial values
|
||||
|
||||
let activeTopologyOptions = null;
|
||||
let adjacentNodes = makeSet();
|
||||
let currentTopology = null;
|
||||
let currentTopologyId = 'containers';
|
||||
let errorUrl = null;
|
||||
@@ -93,6 +95,22 @@ const AppStore = assign({}, EventEmitter.prototype, {
|
||||
return activeTopologyOptions;
|
||||
},
|
||||
|
||||
getAdjacentNodes: function(nodeId) {
|
||||
adjacentNodes = adjacentNodes.clear();
|
||||
|
||||
if (nodes.has(nodeId)) {
|
||||
adjacentNodes = makeSet(nodes.get(nodeId).get('adjacency'));
|
||||
// fill up set with reverse edges
|
||||
nodes.forEach(function(node, id) {
|
||||
if (node.get('adjacency') && node.get('adjacency').includes(nodeId)) {
|
||||
adjacentNodes = adjacentNodes.add(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return adjacentNodes;
|
||||
},
|
||||
|
||||
getCurrentTopology: function() {
|
||||
if (!currentTopology) {
|
||||
currentTopology = setTopology(currentTopologyId);
|
||||
@@ -139,8 +157,8 @@ const AppStore = assign({}, EventEmitter.prototype, {
|
||||
|
||||
getHighlightedNodeIds: function() {
|
||||
if (mouseOverNodeId) {
|
||||
const adjacency = nodes.get(mouseOverNodeId).get('adjacency');
|
||||
if (adjacency) {
|
||||
const adjacency = this.getAdjacentNodes(mouseOverNodeId);
|
||||
if (adjacency.size) {
|
||||
return _.union(adjacency.toJS(), [mouseOverNodeId]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ body {
|
||||
|
||||
g.node {
|
||||
cursor: pointer;
|
||||
transition: opacity .5s ease-in-out;
|
||||
|
||||
&.pseudo {
|
||||
opacity: 0.8;
|
||||
@@ -194,12 +195,22 @@ body {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.blurred {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.edge {
|
||||
transition: opacity .5s ease-in-out;
|
||||
|
||||
&.blurred {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.link {
|
||||
stroke: @text-secondary-color;
|
||||
stroke-width: 1.5px;
|
||||
stroke-width: 1px;
|
||||
fill: none;
|
||||
stroke-opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"object-assign": "^2.0.0",
|
||||
"page": "^1.6.3",
|
||||
"react": "^0.13.3",
|
||||
"react-motion": "^0.2.7",
|
||||
"react-tap-event-plugin": "^0.1.7",
|
||||
"react-tween-state": "0.0.5",
|
||||
"reqwest": "~1.1.5",
|
||||
|
||||
Reference in New Issue
Block a user