Network Map now shows all device neighbors and allows click-through nav
This commit is contained in:
4
Changes
4
Changes
@@ -1,5 +1,9 @@
|
|||||||
0.8 -
|
0.8 -
|
||||||
|
|
||||||
|
[NEW FEATURES]
|
||||||
|
|
||||||
|
* Network Map now shows all device neighbors and allows click-through nav
|
||||||
|
|
||||||
[BUG FIXES]
|
[BUG FIXES]
|
||||||
|
|
||||||
* port cotrol user log check now looks for all actions
|
* port cotrol user log check now looks for all actions
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ sub _add_children {
|
|||||||
next if exists var('seen')->{$c};
|
next if exists var('seen')->{$c};
|
||||||
var('seen')->{$c}++;
|
var('seen')->{$c}++;
|
||||||
push @legit, $c;
|
push @legit, $c;
|
||||||
push @{$ptr}, { name => _get_name($c) };
|
push @{$ptr}, { name => _get_name($c), ip => $c };
|
||||||
}
|
}
|
||||||
|
|
||||||
for (my $i = 0; $i < @legit; $i++) {
|
for (my $i = 0; $i < @legit; $i++) {
|
||||||
@@ -136,17 +136,39 @@ get '/ajax/data/device/netmap' => sub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
my %tree = (
|
my %tree = (
|
||||||
|
ip => $start,
|
||||||
name => _get_name($start),
|
name => _get_name($start),
|
||||||
children => [],
|
children => [],
|
||||||
);
|
);
|
||||||
|
|
||||||
var(seen => {});
|
var(seen => {$start => 1});
|
||||||
_add_children($tree{children}, var('links')->{$start});
|
_add_children($tree{children}, var('links')->{$start});
|
||||||
|
|
||||||
content_type('application/json');
|
content_type('application/json');
|
||||||
return to_json(\%tree);
|
return to_json(\%tree);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ajax '/ajax/data/device/alldevicelinks' => sub {
|
||||||
|
my @devices = schema->resultset('Device')->search({}, {
|
||||||
|
result_class => 'DBIx::Class::ResultClass::HashRefInflator',
|
||||||
|
columns => ['ip', 'dns'],
|
||||||
|
})->all;
|
||||||
|
var(devices => { map { $_->{ip} => $_->{dns} } @devices });
|
||||||
|
|
||||||
|
my $rs = schema->resultset('Virtual::DeviceLinks')->search({}, {
|
||||||
|
result_class => 'DBIx::Class::ResultClass::HashRefInflator',
|
||||||
|
});
|
||||||
|
|
||||||
|
my %tree = ();
|
||||||
|
while (my $l = $rs->next) {
|
||||||
|
push @{ $tree{ _get_name($l->{left_ip} )} },
|
||||||
|
_get_name($l->{right_ip});
|
||||||
|
}
|
||||||
|
|
||||||
|
content_type('application/json');
|
||||||
|
return to_json(\%tree);
|
||||||
|
};
|
||||||
|
|
||||||
# device interface addresses
|
# device interface addresses
|
||||||
ajax '/ajax/content/device/addresses' => sub {
|
ajax '/ajax/content/device/addresses' => sub {
|
||||||
my $ip = param('q');
|
my $ip = param('q');
|
||||||
|
|||||||
@@ -381,8 +381,15 @@ form .clearfix.success input {
|
|||||||
|
|
||||||
.link {
|
.link {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: #ccc;
|
stroke: #eee;
|
||||||
stroke-width: 1.5px;
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.neighbor {
|
||||||
|
fill: none;
|
||||||
|
stroke: #aaa;
|
||||||
|
stroke-width: 2px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||||
|
|||||||
@@ -1,65 +1,172 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
var diameter = window.innerHeight;
|
var winHeight = window.innerHeight;
|
||||||
|
var winWidth = window.innerWidth;
|
||||||
|
|
||||||
var tree = d3.layout.tree()
|
var tree = d3.layout.tree()
|
||||||
.size([360, diameter])
|
.size([360, winHeight])
|
||||||
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
|
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
|
||||||
|
|
||||||
var diagonal = d3.svg.diagonal.radial()
|
// links in the initial tree drawing use this generator
|
||||||
|
var treeLink = d3.svg.diagonal.radial()
|
||||||
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
|
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
|
||||||
|
|
||||||
|
// actual device neighbor links use this generator
|
||||||
|
var neighLink = d3.svg.diagonal.radial();
|
||||||
|
|
||||||
|
// store x,y for all circles on the map
|
||||||
|
var loc = {};
|
||||||
|
// store actual links between all nodes
|
||||||
|
var neighbors_data = {};
|
||||||
|
|
||||||
|
// main SVG background, with support for pan/zoom
|
||||||
var svg = d3.select("#netmap_pane").append("svg")
|
var svg = d3.select("#netmap_pane").append("svg")
|
||||||
.attr("width", window.innerWidth - 50)
|
.attr("width", winWidth - 50)
|
||||||
.attr("height", diameter - 100)
|
.attr("height", winHeight - 100)
|
||||||
.attr("pointer-events", "all")
|
.attr("pointer-events", "all")
|
||||||
.append('g')
|
.append('g')
|
||||||
.call(d3.behavior.zoom().on("zoom", redraw))
|
.call(d3.behavior.zoom().on("zoom", redraw))
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
|
.attr("transform", "translate(" + winHeight / 2 + "," + winHeight / 2 + ")");
|
||||||
|
|
||||||
|
// this is the image background
|
||||||
// FIXME there must be a way to discover the radial tree's size?
|
// FIXME there must be a way to discover the radial tree's size?
|
||||||
svg.append('rect')
|
svg.append('rect')
|
||||||
.attr("x", (0 - (diameter * 2)))
|
.attr("x", (0 - (winHeight * 2)))
|
||||||
.attr('width', "400%")
|
.attr('width', "400%")
|
||||||
.attr("y", (0 - (diameter * 2)))
|
.attr("y", (0 - (winHeight * 2)))
|
||||||
.attr('height', "400%")
|
.attr('height', "400%")
|
||||||
.attr('fill', 'white');
|
.attr('fill', 'white');
|
||||||
|
|
||||||
|
// handle pan and zoom
|
||||||
function redraw() {
|
function redraw() {
|
||||||
// console.log("here", d3.event.translate, d3.event.scale);
|
|
||||||
svg.attr("transform",
|
svg.attr("transform",
|
||||||
"translate(" + d3.event.translate + ")"
|
"translate(" + d3.event.translate + ")"
|
||||||
+ "scale(" + d3.event.scale + ")"
|
+ "scale(" + d3.event.scale + ")"
|
||||||
+ "translate(" + (diameter / 2) + "," + (diameter / 2) + ")");
|
+ "translate(" + (winHeight / 2) + "," + (winHeight / 2) + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
d3.json("[% uri_for('/ajax/data/device/netmap') %]?&q=[% params.q | uri %]", function(error, root) {
|
// save the x,y of an element into the loc dictionary
|
||||||
var nodes = tree.nodes(root),
|
function recordLocation(d,i) {
|
||||||
links = tree.links(nodes);
|
var rect = this.getBoundingClientRect();
|
||||||
|
loc[d.name] = {
|
||||||
|
'x': (rect.left + ((rect.right - rect.left) / 2))
|
||||||
|
,'y': (rect.top + ((rect.bottom - rect.top) / 2))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var link = svg.selectAll(".link")
|
// convert a device name to a valid CSS class name
|
||||||
.data(links)
|
function to_class(name) { return name.replace(/\./g, "_") }
|
||||||
.enter().append("path")
|
|
||||||
.attr("class", "link")
|
|
||||||
.attr("d", diagonal);
|
|
||||||
|
|
||||||
var node = svg.selectAll(".node")
|
// handler for clicking on a circle - redirect to that device's netmap
|
||||||
.data(nodes)
|
function circleClick(d) {
|
||||||
.enter().append("g")
|
window.location = '[% uri_for('/device') %]?tab=netmap&q=' + d.ip;
|
||||||
.attr("class", "node")
|
}
|
||||||
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
|
|
||||||
|
|
||||||
node.append("circle")
|
// handler for mouseover on a circle - show that device's real neighbors
|
||||||
.attr("r", 4.5);
|
function circleOver(d) {
|
||||||
|
$('.link').hide();
|
||||||
|
$('path.' + to_class(d.name)).show();
|
||||||
|
|
||||||
node.append("text")
|
$.each(neighbors_data[d.name], function(idx, target) {
|
||||||
.attr("dy", ".31em")
|
if (! (target in loc)) { return true }
|
||||||
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
|
$('circle.' + to_class(target)).css('fill', '#e96cfa');
|
||||||
.attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; })
|
});
|
||||||
.text(function(d) { return d.name; });
|
}
|
||||||
});
|
|
||||||
|
|
||||||
d3.select(self.frameElement).style("height", diameter - 100 + "px");
|
// handler for mouseout on a circle - hide real neighbours and show treeLinks
|
||||||
|
function circleOut(d) {
|
||||||
|
$.each(neighbors_data[d.name], function(idx, target) {
|
||||||
|
if (! (target in loc)) { return true }
|
||||||
|
$('circle.' + to_class(target)).css('fill', '#fff');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('path.' + to_class(d.name)).hide();
|
||||||
|
$('.link').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// load all device connections into neighbors_data dictionary
|
||||||
|
$.getJSON('[% uri_for('/ajax/data/device/alldevicelinks') %]', function(data) {
|
||||||
|
neighbors_data = data;
|
||||||
|
|
||||||
|
// draw the tree
|
||||||
|
d3.json("[% uri_for('/ajax/data/device/netmap') %]?&q=[% params.q | uri %]", function(error, root) {
|
||||||
|
var nodes = tree.nodes(root),
|
||||||
|
links = tree.links(nodes);
|
||||||
|
|
||||||
|
var link = svg.selectAll(".link")
|
||||||
|
.data(links)
|
||||||
|
.enter().append("path")
|
||||||
|
.attr("class", "link")
|
||||||
|
.attr("d", treeLink);
|
||||||
|
|
||||||
|
var node = svg.selectAll(".node")
|
||||||
|
.data(nodes)
|
||||||
|
.enter().append("g")
|
||||||
|
.attr("class", "node")
|
||||||
|
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; });
|
||||||
|
|
||||||
|
node.append("circle")
|
||||||
|
.attr("r", 4.5)
|
||||||
|
// circle has class name of its device, so we can show/hide it
|
||||||
|
.attr("class", function(d) { return to_class(d.name) })
|
||||||
|
// store the x,y of every circle we've just drawn
|
||||||
|
.each(recordLocation)
|
||||||
|
// handlers for mouse interaction with the circles
|
||||||
|
.on("click", circleClick)
|
||||||
|
.on("mouseover", circleOver)
|
||||||
|
.on("mouseout", circleOut);
|
||||||
|
|
||||||
|
node.append("text")
|
||||||
|
.attr("dy", ".31em")
|
||||||
|
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
|
||||||
|
.attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; })
|
||||||
|
.text(function(d) { return d.name; });
|
||||||
|
|
||||||
|
// reorient text on the root node
|
||||||
|
svg.select(".node")
|
||||||
|
.attr("transform", function(d) {
|
||||||
|
return d.x < 180 ? "rotate(0)translate(0)" : "rotate(180)translate(0)"; });
|
||||||
|
|
||||||
|
// key (name) of the root node in our locations store
|
||||||
|
var rootname = svg.select(".node").data()[0].name;
|
||||||
|
// reformatted neighbors_data for the real neighbor links
|
||||||
|
var neighbors = [];
|
||||||
|
|
||||||
|
// need to build neighbors array only after we have built loc dictionary,
|
||||||
|
// after drawing the circles and storing their x,y
|
||||||
|
$.each(neighbors_data, function(key, val) {
|
||||||
|
if (! (key in loc)) { return true }
|
||||||
|
|
||||||
|
$.each(val, function(idx, name) {
|
||||||
|
if (! (name in loc)) { return true }
|
||||||
|
|
||||||
|
neighbors.push({
|
||||||
|
'source': {
|
||||||
|
'name': key
|
||||||
|
,'x': loc[key]['x']
|
||||||
|
,'y': loc[key]['y']
|
||||||
|
}
|
||||||
|
,'target': {
|
||||||
|
'name': name
|
||||||
|
,'x': loc[name]['x']
|
||||||
|
,'y': loc[name]['y']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// insert Netdisco neighbor links below circles but above tree links
|
||||||
|
svg.selectAll(".neighbor")
|
||||||
|
.data(neighbors)
|
||||||
|
.enter().insert("path", ".node")
|
||||||
|
// add class name of source device, so we can show/hide the link
|
||||||
|
// (also "neighbor" class)
|
||||||
|
.attr("class", function(d) { return ("neighbor " + to_class( d.source.name )) })
|
||||||
|
.attr("d", neighLink)
|
||||||
|
.attr("transform", "translate(-" + loc[rootname]['x'] + ",-" + loc[rootname]['y'] + ")");
|
||||||
|
});
|
||||||
|
|
||||||
|
}); // jquery getJSON for all connections
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user