Browse Source

Tooltips

master
Sebastian Lohff 7 years ago
parent
commit
4cf5383d0d
  1. 1
      .gitignore
  2. 4
      bgpdata/models.py
  3. 143
      bgpdata/templates/bgpdata/map.html
  4. 93
      dnmapper/settings.default.py
  5. 25
      static/css/tipsy.css
  6. 260
      static/js/jquery.tipsy.js
  7. 2
      templates/base.html

1
.gitignore vendored

@ -3,3 +3,4 @@ @@ -3,3 +3,4 @@
.*.swp
.*.swo
db.sqlite3
dnmapper/settings.py

4
bgpdata/models.py

@ -84,6 +84,10 @@ class AS(models.Model): @@ -84,6 +84,10 @@ class AS(models.Model):
def getPeerings(self):
return Peering.objects.filter(Q(as1=self)|Q(as2=self))
def formatLastSeen(self):
if self.lastSeen:
return self.lastSeen.startTime.strftime("%d.%m.%Y %H:%I")
class BorderRouter(models.Model):
# as id, ip, check method, pingable, reachable
# unique: (crawl_id, asno, as id)

143
bgpdata/templates/bgpdata/map.html

@ -25,7 +25,7 @@ @@ -25,7 +25,7 @@
var asdata = [
{% for AS in ASses %}
{id: {{AS.pk}}, nodetype: "AS", asnumber: {{AS.number}}, label: "{{AS.number}}", neighbors: {{AS.getPeerings.count}}, crawled: {%if AS.directlyCrawled%}true{%else%}false{%endif%}, online: {%if AS.online%}true{%else%}false{%endif%}, lastSeenDate: "{{AS.lastSeen}}", lastSeen: {%if AS.lastSeen%}{{AS.lastSeen.pk}}{%else%}null{%endif%}}{%if not forloop.last%},{%endif%}
{id: {{AS.pk}}, nodetype: "AS", asnumber: {{AS.number}}, label: "{{AS.number}}", neighbors: {{AS.getPeerings.count}}, crawled: {%if AS.directlyCrawled%}true{%else%}false{%endif%}, online: {%if AS.online%}true{%else%}false{%endif%}, lastSeenDate: "{{AS.formatLastSeen}}", lastSeen: {%if AS.lastSeen%}{{AS.lastSeen.pk}}{%else%}null{%endif%}, dismiss: true}{%if not forloop.last%},{%endif%}
{%endfor%}
];
@ -63,7 +63,6 @@ var svg = d3.select('#plotwin') @@ -63,7 +63,6 @@ var svg = d3.select('#plotwin')
.attr('width', width)
.attr('height', height);
console.log(asdata);
var force = d3.layout.force()
.nodes(asdata)
.links(peerings)
@ -72,9 +71,7 @@ var force = d3.layout.force() @@ -72,9 +71,7 @@ var force = d3.layout.force()
// .linkDistance(70)
.linkStrength(0.65)
.linkDistance(function(l) {
console.log(l);
neighs = Math.min(l.source.neighbors, l.target.neighbors);
console.log(neighs, "neighbors");
switch(neighs) {
case 0: return 40;
case 1:
@ -103,6 +100,7 @@ var node = svg.selectAll('.node') @@ -103,6 +100,7 @@ var node = svg.selectAll('.node')
.data(asdata)
.enter()
.append("g")
.attr("id", function(d) { return "node-" + d.id; })
.call(force.drag);
node.append("ellipse")
@ -120,6 +118,7 @@ node.append("ellipse") @@ -120,6 +118,7 @@ node.append("ellipse")
.attr("stroke", "black")
.attr("stroke-width", "1px");
node.append('text')
.attr("font-family", "sans-serif")
.attr("font-size", "13px")
@ -128,6 +127,57 @@ node.append('text') @@ -128,6 +127,57 @@ node.append('text')
.attr("text-anchor", "middle")
.text(function(d) { return d.label; });
var lastMouseNode = null;
node.on("mouseover", function(d) {
if(lastMouseNode) {
if(!d.dismiss && lastMouseNode.id == d.id)
return;
else
$("#node-"+lastMouseNode.id).tipsy("hide");
}
d.dismiss = true;
$("#node-"+d.id).tipsy("show");
lastMouseNode = d;
})
.on("mouseout", function(d) {
if(d.dismiss)
$("#node-"+d.id).tipsy("hide");
});
$('svg g').tipsy({
gravity: 'e',
offset: 20,
html: true,
trigger: 'manual',
title: function() {
var d = this.__data__;
var content = '';
if(d.nodetype == 'AS') {
var state = null;
if(d.online)
state = '<span class="label label-success">Online</span>';
else if(d.lastSeen)
state = '<span class="label label-danger">Offline</span>';
else
state = '<span class="label label-default">Never seen</span>';
content = '<b>AS '+d.asnumber+'</b><br>';
content += '<table style="opacity: 1.0" class="table table-bordered">';
content += '<tr><th>State</th><td>'+state+'</td>';
if(d.lastSeen) {
content += '<tr><th>Last seen</th><td><a href="/map/'+d.lastSeen+'/">Crawl '+d.lastSeen+'<br>'+d.lastSeenDate+'</a></td></tr>';
}
if(d.crawled) {
content += '<tr><th>Note</th><td>Directly crawled</td></tr>';
}
content += '</table>';
}
return content;
}
});
force.on("tick", function() {
//node.attr('cx', function(d) { return d.x; })
// .attr('cy', function(d) { return d.y; });
@ -139,51 +189,62 @@ force.on("tick", function() { @@ -139,51 +189,62 @@ force.on("tick", function() {
.attr("y2", function(d) { return d.target.y; });
});
var lastNode = null;
function click(d) {
if(d3.event.defaultPrevented) return;
if(lastNode)
console.log("last time you clicked on another node", d);
//if(lastNode)
// $("#node-" + lastNode.id).tipsy("hide");
d.dismiss = false;
//$("#node-" + d.id).tipsy("show");
$("#infowin").fadeOut('fast', function() {
// set progress bar
$("#infowin").html('<div class="progress"><div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"><span class="sr-only">Loading...</span></div></div>');
console.log("fading done");
var content = '';
if(d.nodetype == 'AS' && d.crawled) {
console.log("doing javascript...");
$.ajax({url: "/map/api/borderrouter/?AS__crawl={{crawl.pk}}&AS__number=" + d.asnumber, success: function(result) {
console.log("SUCCESS", result);
$("#infowin").html('');
$("#infowin").fadeIn('fast', function() {});
for(var i=0; i<result.objects.length; i++) {
(function(currRouter) {
$.ajax({url:"/map/api/announcement/?router="+currRouter.id, success: function(result2) {
console.log("Current router on success", currRouter);
astable = 'Table for AS'+d.asnumber+' router ID ' + currRouter.routerID;
astable += '<table class="table table-bordered">';
astable += '<tr><th>Network</th><th>Next Hop</th><th>AS Path</th></tr>';
for(var j=0; j<result2.objects.length; j++) {
ann = result2.objects[j];
astable += '<tr><td>'+ann.ip+'/'+ann.prefix+'</td><td>'+ann.nextHop+'</td><td>'+ann.ASPath+'</td></tr>';
}
astable += '</table>';
console.log("Sending stuff for", currRouter.id, "aka", currRouter.routerID);
$('#infowin').append(astable);
}});
})(result.objects[i]);
}
}});
astable = '<table class="table table-bordered table-hover">';
astable += '<tr><th scope="row">AS</th><td>'+ d.asnumber +'<td></tr>';
astable += '<tr><th>Directly crawled</th><td><td></tr>';
astable += '<tr><th>Online</th><td><td></tr>';
astable += '<tr><th></th><td><td></tr>';
astable += '</table><div id="announcement"></div>';
if(d.nodetype == 'AS') {
//$("#node-"+d.id).popover({content: "Hallo Katze! :)"});
//$("#node-"+d.id).popover('show');
//console.log("My element", $("#node-"+d.id));
//$("#node-"+d.id).tipsy({
// gravity: 'e',
// html: true,
// //trigger: 'focus',
// trigger: 'hover',
// title: function() {
// var d = this.__data__;
// astable = '<table class="table table-bordered"><tr><td>FOOO</td><td>bar</td></table>';
// return astable;
// }
//});
if(d.crawled) {
$.ajax({url: "/map/api/borderrouter/?AS__crawl={{crawl.pk}}&AS__number=" + d.asnumber, success: function(result) {
$("#infowin").html('');
$("#infowin").fadeIn('fast', function() {});
for(var i=0; i<result.objects.length; i++) {
(function(currRouter) {
$.ajax({url:"/map/api/announcement/?router="+currRouter.id, success: function(result2) {
astable = 'Table for AS'+d.asnumber+' router ID ' + currRouter.routerID;
astable += '<table class="table table-bordered">';
astable += '<tr><th>Network</th><th>Next Hop</th><th>AS Path</th></tr>';
for(var j=0; j<result2.objects.length; j++) {
ann = result2.objects[j];
astable += '<tr><td>'+ann.ip+'/'+ann.prefix+'</td><td>'+ann.nextHop+'</td><td>'+ann.ASPath+'</td></tr>';
}
astable += '</table>';
$('#infowin').append(astable);
}});
})(result.objects[i]);
}
}});
}
} else {
@ -191,10 +252,10 @@ function click(d) { @@ -191,10 +252,10 @@ function click(d) {
$("#infowin").fadeIn('fast', function() {});
}
});
console.log("Single click on", d);
lastNode = d;
}
node.on("click", click)
node.on("click", click);
force.start();

93
dnmapper/settings.default.py

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
"""
Django settings for dnmapper project.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'crv*hfx2pkxvq1s!)dbz*hdu+r7u2$y4djf6_#6mm)shk9e!58'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []
STATICFILES_DIRS = (
'static/',
)
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'bgpdata',
'tastypie',
)
API_LIMIT_PER_PAGE = 100
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'dnmapper.urls'
WSGI_APPLICATION = 'dnmapper.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = 'de-de'
TIME_ZONE = 'CET'
USE_I18N = True
USE_L10N = True
USE_TZ = True
TEMPLATE_DIRS = (
'templates/',
)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/'

25
static/css/tipsy.css

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
.tipsy { font-size: 14px; position: absolute; padding: 5px; z-index: 100000; }
.tipsy-inner { background-color: #000; color: #FFF; max-width: 200px; padding: 5px 8px 4px 8px; text-align: center; }
/* Rounded corners */
.tipsy-inner { border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; }
/* Uncomment for shadow */
/*.tipsy-inner { box-shadow: 0 0 5px #000000; -webkit-box-shadow: 0 0 5px #000000; -moz-box-shadow: 0 0 5px #000000; }*/
.tipsy-arrow { position: absolute; width: 0; height: 0; line-height: 0; border: 5px dashed #000; }
/* Rules to colour arrows */
.tipsy-arrow-n { border-bottom-color: #000; }
.tipsy-arrow-s { border-top-color: #000; }
.tipsy-arrow-e { border-left-color: #000; }
.tipsy-arrow-w { border-right-color: #000; }
.tipsy-n .tipsy-arrow { top: 0px; left: 50%; margin-left: -5px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent; }
.tipsy-nw .tipsy-arrow { top: 0; left: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;}
.tipsy-ne .tipsy-arrow { top: 0; right: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;}
.tipsy-s .tipsy-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; }
.tipsy-sw .tipsy-arrow { bottom: 0; left: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; }
.tipsy-se .tipsy-arrow { bottom: 0; right: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; }
.tipsy-e .tipsy-arrow { right: 0; top: 50%; margin-top: -5px; border-left-style: solid; border-right: none; border-top-color: transparent; border-bottom-color: transparent; }
.tipsy-w .tipsy-arrow { left: 0; top: 50%; margin-top: -5px; border-right-style: solid; border-left: none; border-top-color: transparent; border-bottom-color: transparent; }

260
static/js/jquery.tipsy.js

@ -0,0 +1,260 @@ @@ -0,0 +1,260 @@
// tipsy, facebook style tooltips for jquery
// version 1.0.0a
// (c) 2008-2010 jason frame [jason@onehackoranother.com]
// released under the MIT license
(function($) {
function maybeCall(thing, ctx) {
return (typeof thing == 'function') ? (thing.call(ctx)) : thing;
};
function isElementInDOM(ele) {
while (ele = ele.parentNode) {
if (ele == document) return true;
}
return false;
};
function Tipsy(element, options) {
this.$element = $(element);
this.options = options;
this.enabled = true;
this.fixTitle();
};
Tipsy.prototype = {
show: function() {
var title = this.getTitle();
if (title && this.enabled) {
var $tip = this.tip();
$tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title);
$tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity
$tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body);
var pos = $.extend({}, this.$element.offset(), {
width: this.$element[0].offsetWidth,
height: this.$element[0].offsetHeight
});
var actualWidth = $tip[0].offsetWidth,
actualHeight = $tip[0].offsetHeight,
gravity = maybeCall(this.options.gravity, this.$element[0]);
var tp;
switch (gravity.charAt(0)) {
case 'n':
tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
break;
case 's':
tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
break;
case 'e':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset};
// XXX HACK: i want to set another offset so apparently I have to hardcode this into the code. obviously.
tp = {top: pos.top + pos.height / 2 - actualHeight / 2 + this.options.offset, left: pos.left - actualWidth};
break;
case 'w':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset};
break;
}
if (gravity.length == 2) {
if (gravity.charAt(1) == 'w') {
tp.left = pos.left + pos.width / 2 - 15;
} else {
tp.left = pos.left + pos.width / 2 - actualWidth + 15;
}
}
$tip.css(tp).addClass('tipsy-' + gravity);
$tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0);
if (this.options.className) {
$tip.addClass(maybeCall(this.options.className, this.$element[0]));
}
if (this.options.fade) {
$tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity});
} else {
$tip.css({visibility: 'visible', opacity: this.options.opacity});
}
}
},
hide: function() {
if (this.options.fade) {
this.tip().stop().fadeOut(function() { $(this).remove(); });
} else {
this.tip().remove();
}
},
fixTitle: function() {
var $e = this.$element;
if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') {
$e.attr('original-title', $e.attr('title') || '').removeAttr('title');
}
},
getTitle: function() {
var title, $e = this.$element, o = this.options;
this.fixTitle();
var title, o = this.options;
if (typeof o.title == 'string') {
title = $e.attr(o.title == 'title' ? 'original-title' : o.title);
} else if (typeof o.title == 'function') {
title = o.title.call($e[0]);
}
title = ('' + title).replace(/(^\s*|\s*$)/, "");
return title || o.fallback;
},
tip: function() {
if (!this.$tip) {
this.$tip = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>');
this.$tip.data('tipsy-pointee', this.$element[0]);
}
return this.$tip;
},
validate: function() {
if (!this.$element[0].parentNode) {
this.hide();
this.$element = null;
this.options = null;
}
},
enable: function() { this.enabled = true; },
disable: function() { this.enabled = false; },
toggleEnabled: function() { this.enabled = !this.enabled; }
};
$.fn.tipsy = function(options) {
if (options === true) {
return this.data('tipsy');
} else if (typeof options == 'string') {
var tipsy = this.data('tipsy');
if (tipsy) tipsy[options]();
return this;
}
options = $.extend({}, $.fn.tipsy.defaults, options);
function get(ele) {
var tipsy = $.data(ele, 'tipsy');
if (!tipsy) {
tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options));
$.data(ele, 'tipsy', tipsy);
}
return tipsy;
}
function enter() {
var tipsy = get(this);
tipsy.hoverState = 'in';
if (options.delayIn == 0) {
tipsy.show();
} else {
tipsy.fixTitle();
setTimeout(function() { if (tipsy.hoverState == 'in') tipsy.show(); }, options.delayIn);
}
};
function leave() {
var tipsy = get(this);
tipsy.hoverState = 'out';
if (options.delayOut == 0) {
tipsy.hide();
} else {
setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut);
}
};
if (!options.live) this.each(function() { get(this); });
if (options.trigger != 'manual') {
var binder = options.live ? 'live' : 'bind',
eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus',
eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur';
this[binder](eventIn, enter)[binder](eventOut, leave);
}
return this;
};
$.fn.tipsy.defaults = {
className: null,
delayIn: 0,
delayOut: 0,
fade: false,
fallback: '',
gravity: 'n',
html: false,
live: false,
offset: 0,
opacity: 0.8,
title: 'title',
trigger: 'hover'
};
$.fn.tipsy.revalidate = function() {
$('.tipsy').each(function() {
var pointee = $.data(this, 'tipsy-pointee');
if (!pointee || !isElementInDOM(pointee)) {
$(this).remove();
}
});
};
// Overwrite this method to provide options on a per-element basis.
// For example, you could store the gravity in a 'tipsy-gravity' attribute:
// return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' });
// (remember - do not modify 'options' in place!)
$.fn.tipsy.elementOptions = function(ele, options) {
return $.metadata ? $.extend({}, options, $(ele).metadata()) : options;
};
$.fn.tipsy.autoNS = function() {
return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n';
};
$.fn.tipsy.autoWE = function() {
return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w';
};
/**
* yields a closure of the supplied parameters, producing a function that takes
* no arguments and is suitable for use as an autogravity function like so:
*
* @param margin (int) - distance from the viewable region edge that an
* element should be before setting its tooltip's gravity to be away
* from that edge.
* @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer
* if there are no viewable region edges effecting the tooltip's
* gravity. It will try to vary from this minimally, for example,
* if 'sw' is preferred and an element is near the right viewable
* region edge, but not the top edge, it will set the gravity for
* that element's tooltip to be 'se', preserving the southern
* component.
*/
$.fn.tipsy.autoBounds = function(margin, prefer) {
return function() {
var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)},
boundTop = $(document).scrollTop() + margin,
boundLeft = $(document).scrollLeft() + margin,
$this = $(this);
if ($this.offset().top < boundTop) dir.ns = 'n';
if ($this.offset().left < boundLeft) dir.ew = 'w';
if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e';
if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's';
return dir.ns + (dir.ew ? dir.ew : '');
}
};
})(jQuery);

2
templates/base.html

@ -4,10 +4,12 @@ @@ -4,10 +4,12 @@
<head>
<link href="{% static "css/bootstrap.min.css" %}" rel="stylesheet">
<link href="{% static "css/main.css" %}" rel="stylesheet">
<link href="{% static "css/tipsy.css" %}" rel="stylesheet">
{% block head %}{% endblock %}
</head>
<body>
<script src="{% static "js/jquery.min.js" %}"></script>
<script src="{% static "js/jquery.tipsy.js" %}"></script>
<script src="{% static "js/bootstrap.min.js" %}"></script>
<!-- Static navbar -->

Loading…
Cancel
Save