├── LICENSE ├── README.md ├── dhmon.html ├── dhmon.js ├── localserver.py ├── src ├── dhmap.js ├── dhmenu.js ├── examples │ ├── data.json │ ├── example.js │ └── index.html ├── ipplan2dhmap.py └── vendor │ ├── jquery │ ├── MIT-LICENSE.txt │ └── jquery-2.1.1.min.js │ ├── jqueryui │ ├── images │ │ ├── ui-bg_diagonals-thick_18_b81900_40x40.png │ │ ├── ui-bg_diagonals-thick_20_666666_40x40.png │ │ ├── ui-bg_flat_10_000000_40x100.png │ │ ├── ui-bg_glass_100_f6f6f6_1x400.png │ │ ├── ui-bg_glass_100_fdf5ce_1x400.png │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ ├── ui-bg_gloss-wave_35_f6a828_500x100.png │ │ ├── ui-bg_highlight-soft_100_eeeeee_1x100.png │ │ ├── ui-bg_highlight-soft_75_ffe45c_1x100.png │ │ ├── ui-icons_222222_256x240.png │ │ ├── ui-icons_228ef1_256x240.png │ │ ├── ui-icons_ef8c08_256x240.png │ │ ├── ui-icons_ffd27a_256x240.png │ │ └── ui-icons_ffffff_256x240.png │ ├── jquery-ui.min.css │ ├── jquery-ui.min.js │ ├── jquery-ui.structure.min.css │ └── jquery-ui.theme.min.css │ ├── raphael-zpd │ └── raphael-zpd.js │ └── raphael │ ├── MIT-LICENSE.txt │ └── raphael-2.1.2.min.js ├── whereami.html └── whereami.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dreamhack Tech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dhmap 2 | ===== 3 | 4 | HTML5/JS library for drawing and updating network layouts 5 | 6 | # Try it 7 | 8 | cd src/ 9 | python -m SimpleHTTPServer 10 | 11 | Go to `http://localhost:8000/examples/` 12 | -------------------------------------------------------------------------------- /dhmon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | dhmap 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 164 | 165 | 166 | 175 |
176 | 182 |
183 | 184 | 185 | -------------------------------------------------------------------------------- /dhmon.js: -------------------------------------------------------------------------------- 1 | var switch_status = {}; 2 | var map = null; 3 | 4 | var ping = null; 5 | var snmp = null; 6 | var model = null; 7 | var iface = null; 8 | var switch_vlans = null; 9 | var dhcp_status = null; 10 | var start_fetch = null; 11 | var alert_hosts = null; 12 | 13 | var ifre =/(^[gxf]e-[0-9\/]+$)|(Ethernet)/; 14 | var gere =/(^ge-[0-9\/]+$)|(GigabitEthernet)/; 15 | 16 | var errors_to_human = { 17 | 'OK': 'Everything working as expected', 18 | 'SPEED': 'At least one link is running at non-ideal speed or link is full', 19 | 'STP': 'At least one port is blocked by Spanning Tree', 20 | 'ERRORS': 'At least one port is dropping packets due to corruption', 21 | 'WARNING': 'The switch is not replying to SNMP requests', 22 | 'CRITICAL': 'The switch has not replied to ICMP for 30 seconds or more', 23 | 'ALERT': 'The switch has at least one alert present in the monitoring system' 24 | }; 25 | 26 | var openDialog; 27 | var openDialogName; 28 | 29 | // TODO(bluecmd): Yeah, event mode makes you write the best code. 30 | // Refactor later (TM). 31 | function checkIfaceSpeed(sw, model, ifaces) { 32 | if (model == undefined || ifaces == undefined) 33 | return true; 34 | 35 | var failed = false; 36 | for (var name in ifaces) { 37 | var iface = ifaces[name]; 38 | /* skip access ports, sleeping computers rarely link max speed */ 39 | if (!iface.trunk) 40 | continue; 41 | 42 | if (ifre.exec(name) == null) { 43 | continue; 44 | } 45 | 46 | if (iface.status == 'up') { 47 | if (iface.speed == '10') { 48 | failed = true; 49 | } else if (iface.speed == '100') { 50 | if (gere.exec(name) == null) { 51 | failed = true; 52 | } 53 | } 54 | } 55 | } 56 | return !failed; 57 | } 58 | 59 | function checkIfaceErrors(sw, model, ifaces) { 60 | if (model == undefined || ifaces == undefined) 61 | return true; 62 | 63 | var failed = false; 64 | var show_consumer_ifaces = 65 | document.getElementById('hilight_consumer_issues').checked; 66 | for (var name in ifaces) { 67 | var iface = ifaces[name]; 68 | 69 | /* skip access ports if we don't want to show consumer ifaces */ 70 | if (!iface.trunk && !show_consumer_ifaces) 71 | continue; 72 | 73 | if (ifre.exec(name) == null) { 74 | continue; 75 | } 76 | 77 | if (iface.status == 'up') { 78 | if (iface.errors_in > 0 || iface.errors_out > 0) { 79 | failed = true; 80 | } 81 | } 82 | } 83 | return !failed; 84 | } 85 | 86 | function checkIfaceStp(sw, model, ifaces) { 87 | if (model == undefined || ifaces == undefined) 88 | return true; 89 | 90 | var failed = false; 91 | var show_consumer_ifaces = 92 | document.getElementById('hilight_consumer_issues').checked; 93 | 94 | for (var name in ifaces) { 95 | var iface = ifaces[name]; 96 | /* skip access ports if we don't want to show consumer ifaces */ 97 | if (!iface.trunk && !show_consumer_ifaces) 98 | continue; 99 | 100 | /* TODO(bluecmd): Maybe not set 'error' on non-ethernet 101 | * interfaces that don't speak STP */ 102 | if (ifre.exec(name) == null) { 103 | continue; 104 | } 105 | 106 | if (iface.status == 'up') { 107 | if (iface.stp == 'error') { 108 | failed = true; 109 | } 110 | } 111 | } 112 | return !failed; 113 | } 114 | 115 | function computeStatus() { 116 | if (iface == null || model == null || snmp == null || ping == null) 117 | return; 118 | 119 | var now = new Date(); 120 | var latency = now - start_fetch; 121 | console.log( 122 | '[' + now.toLocaleString() + '] Loaded new data in ' + latency + 'ms'); 123 | 124 | switch_status = {}; 125 | 126 | for (var sw in ping) { 127 | if (ping[sw] >= 60) { 128 | switch_status[sw] = 'CRITICAL'; 129 | } else if (!checkIfaceStp(sw, model[sw], iface[sw])) { 130 | switch_status[sw] = 'STP'; 131 | } else if (!checkIfaceSpeed(sw, model[sw], iface[sw])) { 132 | switch_status[sw] = 'SPEED'; 133 | } else if (!checkIfaceErrors(sw, model[sw], iface[sw])) { 134 | switch_status[sw] = 'ERRORS'; 135 | } else if (snmp[sw] == undefined || snmp[sw].since > 360) { 136 | switch_status[sw] = 'WARNING'; 137 | } else if (alert_hosts[sw] != undefined) { 138 | switch_status[sw] = 'ALERT'; 139 | } else { 140 | switch_status[sw] = 'OK'; 141 | } 142 | var swname = sw.split('.')[0]; 143 | 144 | if(openDialogName == swname) 145 | updateSwitchDialog(swname, sw); 146 | } 147 | dhmap.updateSwitches(switch_status); 148 | dhmenu.updateSwitches(switch_status); 149 | } 150 | 151 | function click(sw) { 152 | 153 | // Close open dialog 154 | if(openDialog){ 155 | openDialog.dialog('destroy').remove() 156 | openDialog = undefined; 157 | openDialogName = undefined; 158 | } 159 | 160 | var title = ''; 161 | var swname = sw.name.split('.')[0]; 162 | title += '
'; 163 | title += swname.toUpperCase(); 164 | var dialog = $('
').attr({'title': title}); 165 | dialog.append($('').attr({'id': 'info-' + swname})); 166 | dialog.append($('
').attr({'id': 'dhcpinfo-' + swname})); 167 | dialog.append($('
').attr({'id': 'ports-' + swname})); 168 | dialog.append($('
')); 169 | dialog.append($('
').attr({'id': 'portinfo-' + swname})); 170 | dialog.dialog({width: 500, height: 390, resizable: false, 171 | close: function() { 172 | $(this).dialog('destroy').remove() 173 | openDialog = undefined; 174 | openDialogName = undefined; 175 | }}); 176 | 177 | openDialog = dialog; 178 | openDialogName = swname; 179 | updateSwitchDialog(swname, sw.name); 180 | } 181 | 182 | function updateSwitchDialog(sw, fqdn) { 183 | var div = $('#switch-' + sw); 184 | if (div == undefined || !iface || iface[fqdn] == undefined) 185 | return 186 | div.css({'background-color': dhmap.colour[switch_status[fqdn]]}); 187 | 188 | var info = $('#info-' + sw); 189 | info.html('

Status: ' + errors_to_human[switch_status[fqdn]] + '

'); 190 | 191 | var dhcpinfo = $('#dhcpinfo-' + sw); 192 | dhcpinfo.html(''); 193 | var dhcptable = $(''); 194 | if (switch_vlans[fqdn] != undefined) { 195 | dhcptable.append( 196 | ''); 197 | for (var vlan in switch_vlans[fqdn]) { 198 | // Grab the first network with the same VLAN 199 | for (var network in dhcp_status) { 200 | var ds = dhcp_status[network]; 201 | if (ds.vlan == vlan) { 202 | dhcptable.append( 203 | $('') 204 | .append($('
NetworkClientsMaxDHCP Pool Usage
').text(network)) 205 | .append($('').text(ds.usage)) 206 | .append($('').text(ds.max)) 207 | .append($('').text(Math.ceil(ds.usage / ds.max * 100) + '%'))) 208 | } 209 | } 210 | } 211 | dhcpinfo.append(dhcptable); 212 | } else { 213 | dhcpinfo.append('No DHCP pool data available because VLAN information unavailable for switch model'); 214 | } 215 | 216 | var ports = $('#ports-' + sw); 217 | ports.html('
'); 218 | 219 | var portsdiv = $('
'); 220 | 221 | var order = {}; 222 | for (var idx in iface[fqdn]) { 223 | order[parseInt(iface[fqdn][idx].lastoid)] = idx; 224 | } 225 | 226 | function sortNum(a, b) { 227 | return a - b; 228 | } 229 | 230 | var count = 0; 231 | var key_order = Object.keys(order).sort(sortNum); 232 | for (var kidx in key_order) { 233 | var idx = order[key_order[kidx]]; 234 | var entry = iface[fqdn][idx]; 235 | var ifacename = idx; 236 | 237 | /* Skip special interfaces */ 238 | if (ifre.exec(ifacename) == null) { 239 | continue; 240 | } 241 | 242 | count++; 243 | if (count % 24 == 1 && count > 1) 244 | portsdiv.append('
'); 245 | 246 | var portdiv = $('
').attr({ 247 | 'id': 'port-' + sw + ':' + entry.lastoid}); 248 | 249 | tx_full = false; 250 | rx_full = false; 251 | speed_fail = false; 252 | if (entry.status == 'up') { 253 | portdiv.css({'background-color': dhmap.colour.OK}); 254 | tx_full = ((entry.tx_10min * 8 / 1000 / 1000) / entry.speed > 0.95); 255 | rx_full = ((entry.rx_10min * 8 / 1000 / 1000) / entry.speed > 0.95); 256 | speed_fail = (entry.trunk && parseInt(entry.speed) < 1000) || 257 | (!entry.trunk && parseInt(entry.speed) < 100); 258 | if (parseFloat(entry.errors_in) > 0) 259 | portdiv.css({'background-color': dhmap.colour.ERRORS}); 260 | if (parseFloat(entry.errors_out) > 0) 261 | portdiv.css({'background-color': dhmap.colour.ERRORS}); 262 | if (tx_full || rx_full || speed_fail) 263 | portdiv.css({'background-color': dhmap.colour.SPEED}); 264 | if (!entry.trunk && parseInt(entry.speed) < 1000 && 265 | gere.exec(ifacename) != null) 266 | portdiv.css({'background-color': dhmap.colour.SPEED}); 267 | if (entry.stp == 'error') 268 | portdiv.css({'background-color': dhmap.colour.STP}); 269 | } 270 | if (entry.admin != 'up') 271 | portdiv.css({'background-color': dhmap.colour.WARNING}); 272 | 273 | portdiv.hover(function(entry, ifacename, sw, tx_full, rx_full, speed_fail) { 274 | var portinfo = $('#portinfo-' + sw); 275 | portinfo.html(''); 276 | 277 | var table = $(''); 278 | table.append( 279 | $('').append('')); 281 | table.append( 282 | $('').append('')); 283 | if (entry.admin != 'up') { 284 | table.append($('').append( 285 | '')); 286 | } 287 | if (entry.status == 'up') { 288 | table.append( 289 | $('').append('')); 290 | sb = speed_fail ? 'font-weight: bold; animation: blinker 1s linear infinite;' : ''; 291 | table.append( 292 | $('').append('')); 293 | tb = tx_full ? 'font-weight: bold; animation: blinker 1s linear infinite;' : ''; 294 | rb = rx_full ? 'font-weight: bold; animation: blinker 1s linear infinite;' : ''; 295 | table.append( 296 | $('').append('')); 299 | table.append( 300 | $('').append('')); 303 | table.append( 304 | $('').append( 305 | '')); 307 | } 308 | 309 | portinfo.append(table); 310 | $(this).css({'border-color': 'red'}); 311 | }.bind(portdiv, entry, ifacename, sw, tx_full, rx_full, speed_fail), function() { 312 | $(this).css({'border-color': 'black'}); 313 | }.bind(portdiv)); 314 | portsdiv.append(portdiv); 315 | } 316 | 317 | ports.append(portsdiv); 318 | } 319 | 320 | 321 | $.getJSON('./data.json', function(objects) { 322 | dhmap.init(objects, click); 323 | dhmenu.init(objects, click); 324 | 325 | function updateStatus() { 326 | start_fetch = new Date(); 327 | $.when( 328 | $.getJSON('/analytics/ping.status', function(objects) { 329 | ping = objects; 330 | }), 331 | $.getJSON('/analytics/snmp.saves', function(objects) { 332 | snmp = objects; 333 | }), 334 | $.getJSON('/analytics/switch.model', function(objects) { 335 | model = objects; 336 | }), 337 | $.getJSON('/analytics/switch.interfaces', function(objects) { 338 | iface = objects; 339 | }), 340 | $.getJSON('/analytics/dhcp.status', function(objects) { 341 | dhcp_status = objects; 342 | }), 343 | $.getJSON('/analytics/switch.vlans', function(objects) { 344 | switch_vlans = objects; 345 | }), 346 | $.getJSON('/analytics/alerts.hosts', function(objects) { 347 | alert_hosts = objects; 348 | }) 349 | ).then(function() { 350 | computeStatus(); 351 | }); 352 | } 353 | setInterval(updateStatus, 10000); 354 | updateStatus(); 355 | }); 356 | 357 | 358 | // Allow HTML in the dialog title 359 | $.widget("ui.dialog", $.extend({}, $.ui.dialog.prototype, { 360 | _title: function(title) { 361 | if (!this.options.title ) { 362 | title.html(" "); 363 | } else { 364 | title.html(this.options.title); 365 | } 366 | } 367 | })); 368 | 369 | function darkmode(mode) { 370 | document.body.style.backgroundColor = mode ? '#111' : '#fff'; 371 | } 372 | -------------------------------------------------------------------------------- /localserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Used to run dhmap + analytics on the same machine for dev purposes 3 | 4 | import SimpleHTTPServer 5 | import SocketServer 6 | 7 | PORT = 8000 8 | 9 | class RevHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 10 | def do_GET(self): 11 | if self.path.startswith('/analytics/'): 12 | path = self.path[len('/analytics/'):] 13 | self.send_response(302) 14 | self.send_header("Location", "http://localhost:5000/" + path) 15 | self.end_headers() 16 | return None 17 | else: 18 | SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 19 | 20 | httpd = SocketServer.TCPServer(("", PORT), RevHandler) 21 | 22 | print("serving at port {}".format(PORT)) 23 | httpd.serve_forever() 24 | 25 | -------------------------------------------------------------------------------- /src/dhmap.js: -------------------------------------------------------------------------------- 1 | // ┌────────────────────────────────────────────────────────────────────┐ \\ 2 | // │ dhmap - HTML5/JS network layouts │ \\ 3 | // ├────────────────────────────────────────────────────────────────────┤ \\ 4 | // │ Copyright © 2014 Dreamhack Tech │ \\ 5 | // │ Copyright © 2014 Niklas Lindblad │ \\ 6 | // ├────────────────────────────────────────────────────────────────────┤ \\ 7 | // │ Licensed under the MIT license. │ \\ 8 | // └────────────────────────────────────────────────────────────────────┘ \\ 9 | 10 | var dhmap = {}; 11 | 12 | (function () { 13 | var scaling = 3; 14 | var switches = {}; 15 | var paper = null; 16 | var canvasObjects = {}; 17 | var offsetX = 0; 18 | var offsetY = 0; 19 | var boundingX = 0; 20 | var boundingY = 0; 21 | var onclick = null; 22 | 23 | var defaultMatrix = 'matrix(0.40473940968513483,0,0,0.40473940968513483,77.97065002983072,118.90358368190753)'; 24 | 25 | dhmap.colour = { 26 | 'OK': 'rgb(137,245,108)', 27 | 'CRITICAL':'rgb(255,0,0)', 28 | 'WARNING': 'rgb(255,191,0)', 29 | 'SPEED': 'rgb(223,0,255)', 30 | 'STP': 'rgb(0,102,153)', 31 | 'ERRORS': 'rgb(0,255,255)', 32 | 'ALERT': 'rgb(255,200,0)', 33 | 'UNKNOWN': 'rgb(112,112,112)', 34 | 'TABLE': 'rgba(242,242,242, 0.5)' 35 | }; 36 | 37 | // Draw a rectangle and save a reference to the object 38 | function renderRectangle(object, fillColor, isTable, dry) { 39 | // Whether the object is horizontal or not, determines whether to swap 40 | // width/height to conform with RaphaelJS drawing 41 | var width = object.horizontal == 1 ? object.width : object.height; 42 | var height = object.horizontal == 1 ? object.height : object.width; 43 | 44 | // See if this will increase the bounding box 45 | boundingX = Math.max(object.x1 + width, boundingX); 46 | boundingY = Math.max(object.y1 + height, boundingY); 47 | 48 | // Scale according to the scaling factor defined at the top 49 | width = width * scaling; 50 | height = height * scaling; 51 | var x1 = (offsetX + object.x1) * scaling; 52 | var y1 = (offsetY + object.y1) * scaling; 53 | 54 | if (dry) { 55 | return; 56 | } 57 | 58 | // Create the rectangle and fill it 59 | var rectangle = paper.rect(x1, y1, width, height); 60 | rectangle.attr({fill: fillColor}); 61 | 62 | var shortName = object.name.split('.')[0].toUpperCase(); 63 | 64 | // Table 65 | if (isTable) { 66 | // Add a label 67 | if(object.horizontal == 0){ 68 | var labelOffsetX = 11; 69 | var labelOffsetY = 23; 70 | } 71 | else { 72 | var labelOffsetX = 23; 73 | var labelOffsetY = 13; 74 | } 75 | rectangle.label = paper.text(rectangle.attr('x') + labelOffsetX, rectangle.attr('y') + labelOffsetY, shortName); 76 | rectangle.label.attr({"font-size": 22}); 77 | if(object.horizontal == 0 && isTable) { 78 | rectangle.label.rotate(90); 79 | } 80 | } 81 | 82 | // Device 83 | else { 84 | // Add a label 85 | var labelOffsetX = 5; 86 | var labelOffsetY = -37; 87 | rectangle.labelbox = paper.rect( 88 | rectangle.attr('x') + labelOffsetX, 89 | rectangle.attr('y') + labelOffsetY, 90 | width+50, 91 | width+10); 92 | rectangle.labelbox.attr({fill: "#fff"}); 93 | rectangle.labelbox.label = paper.text( 94 | rectangle.attr('x') + labelOffsetX + 38, 95 | rectangle.attr('y') + labelOffsetY + 20, 96 | shortName); 97 | rectangle.labelbox.label.attr({"font-size": 22}); 98 | } 99 | 100 | // For some objects it might be desirable to hide the label. 101 | // For those objects we add a mouse over to display it. 102 | if ( ! isTable ) { 103 | rectangle.labelbox.hide(); 104 | rectangle.labelbox.label.hide(); 105 | rectangle.mouseover(function() { 106 | this.labelbox.show(); 107 | this.labelbox.label.show(); 108 | this.labelbox.toFront(); 109 | this.labelbox.label.toFront(); 110 | this.attr({"cursor": "pointer"}); 111 | }); 112 | rectangle.mouseout(function() { 113 | this.labelbox.hide(); 114 | this.labelbox.label.hide(); 115 | this.attr({"cursor": "default"}); 116 | }); 117 | rectangle.click(function() { 118 | onclick(object); 119 | }); 120 | } 121 | 122 | // Add rectangle to list of all drawn objects 123 | canvasObjects[object.name] = rectangle; 124 | } 125 | 126 | // Draw a network switch. Defaults to amber colour 127 | function renderSwitch(object, dry) { 128 | object.width = object.width + 1.7; 129 | object.height = object.height + 1.7; 130 | object.x1 = object.x1 - 1; 131 | object.y1 = object.y1 - 0.7; 132 | switches[object.name] = object; 133 | renderRectangle(object, dhmap.colour.UNKNOWN, false, dry); 134 | } 135 | 136 | // Draw a table 137 | function renderTable(object, dry) { 138 | renderRectangle(object, dhmap.colour.TABLE, true, dry); 139 | } 140 | 141 | // Update colour of previously drawn switch 142 | function setSwitchColor(name, color) { 143 | if (canvasObjects[name].attrs.fill != color && canvasObjects[name].attrs.fill != "#0000ff") { 144 | canvasObjects[name].attr({"fill": color}) 145 | } 146 | } 147 | 148 | dhmap.oldTableObject; 149 | dhmap.oldTableColor; 150 | dhmap.oldSwitchObject; 151 | dhmap.oldSwitchColor; 152 | dhmap.filter = function(searchFor) { 153 | 154 | // Reset previously marked 155 | if(dhmap.oldTableObject){ 156 | dhmap.oldTableObject.attr({"fill": dhmap.oldTableColor}); 157 | } 158 | if(dhmap.oldSwitchObject){ 159 | dhmap.oldSwitchObject.attr({"fill": dhmap.oldSwitchColor}); 160 | } 161 | 162 | // Mark object if not already marked 163 | if(searchFor){ 164 | 165 | // Table 166 | if(canvasObjects[searchFor.toUpperCase()] 167 | && canvasObjects[searchFor.toUpperCase()] != dhmap.oldTableObject){ 168 | dhmap.oldSwitchObject = undefined; 169 | dhmap.oldTableObject = canvasObjects[searchFor.toUpperCase()]; 170 | } 171 | // Switch 172 | else if (canvasObjects[searchFor.toLowerCase()+'.event.dreamhack.local'] 173 | && canvasObjects[searchFor.toLowerCase()+'.event.dreamhack.local'] != dhmap.oldSwitchObject){ 174 | dhmap.oldSwitchObject = canvasObjects[searchFor.toLowerCase()+'.event.dreamhack.local']; 175 | dhmap.oldTableObject = canvasObjects[searchFor.toUpperCase().substr(0, searchFor.indexOf('-'))]; 176 | } 177 | else{ 178 | dhmap.oldSwitchObject = undefined; 179 | dhmap.oldTableObject = undefined; 180 | return; 181 | } 182 | 183 | if(dhmap.oldTableObject){ 184 | dhmap.oldTableColor = dhmap.oldTableObject.attr("fill"); 185 | dhmap.oldTableObject.attr({"fill": "#0000ff"}); 186 | } 187 | if(dhmap.oldSwitchObject){ 188 | dhmap.oldSwitchColor = dhmap.oldSwitchObject.attr("fill"); 189 | dhmap.oldSwitchObject.attr({"fill": "#0000ff"}); 190 | } 191 | } 192 | } 193 | 194 | // Update the status of all switches previously drawn on screen 195 | dhmap.updateSwitches = function(statuses) { 196 | // Save state 197 | lastStatuses = statuses; 198 | for ( var name in switches ) { 199 | // If the switch is unknown, render it as amber 200 | if ( statuses[name] === undefined ) { 201 | setSwitchColor(name, dhmap.colour.UNKNOWN); 202 | } else { 203 | // A confirmed healthy switch is green, failed ones are red 204 | setSwitchColor(name, dhmap.colour[statuses[name]]); 205 | } 206 | } 207 | } 208 | 209 | // Which render method to use for each object type 210 | var renders = { 'switch': renderSwitch, 'table': renderTable }; 211 | 212 | dhmap.init = function(objects, click_callback) { 213 | var canvas = document.getElementById('canvas'); 214 | var menu = document.getElementById('menu_container'); 215 | var header = document.getElementById('header'); 216 | canvas.innerHTML = ''; 217 | canvas.style.marginLeft = menu.clientWidth; 218 | 219 | // TODO(bluecmd): Calculate these automatically 220 | var width = window.innerWidth - menu.clientWidth - 2; 221 | var height = window.innerHeight - header.clientHeight - 2; 222 | 223 | if (width < 2000) { 224 | width = 2000; 225 | } 226 | if (height < 800) { 227 | height = 800; 228 | } 229 | 230 | canvas.style.width = width; 231 | canvas.style.height = height; 232 | 233 | menu.style.height = window.innerHeight - header.clientHeight; 234 | paper = Raphael(canvas); 235 | var zpd = new RaphaelZPD(paper, { zoom: true, pan: true, drag: false }); 236 | zpd.gelem.setAttribute('transform', defaultMatrix); 237 | 238 | onclick = click_callback; 239 | switches = {}; 240 | canvasObjects = {}; 241 | 242 | offsetX = 0; 243 | offsetY = 0; 244 | 245 | // First pass: Calculate hall sizes. 246 | // This is to have a simple heuristic to group small halls together. 247 | 248 | var hallsizes = {}; 249 | var hallsizelist = []; 250 | for ( var hall in objects ) { 251 | 252 | if (hall.toLowerCase() == "dist" || hall.toLowerCase() == "prod") 253 | continue; 254 | 255 | // Calculate new bounding box for this hall 256 | // First run is a dry run for just calculating the bounding boxes 257 | boundingX = 0; 258 | boundingY = 0; 259 | 260 | for ( var i in objects[hall] ) { 261 | renders[objects[hall][i]['class']](objects[hall][i], true); 262 | } 263 | 264 | hallsizes[hall] = { 265 | 'x': offsetX, 266 | 'y': offsetY, 267 | 'w': boundingX, 268 | 'h': boundingY, 269 | }; 270 | 271 | hallsizelist.push([boundingY * boundingX, hall]); 272 | } 273 | 274 | // Second phase: Figure out hall order. 275 | hallsizelist.sort(function(a, b) { return a[0] - b[0]; }); 276 | hallsizelist.reverse(); 277 | 278 | var hallorder = []; 279 | for ( var i in hallsizelist ) { 280 | hallorder.push(hallsizelist[i][1]); 281 | } 282 | 283 | // Third pass: Order halls in size order. 284 | // Try to compact halls if possible. 285 | var maxHallHeight = hallsizes[hallorder[0]].h; 286 | var maxX = 0; 287 | var padY = 100; 288 | var padX = 100; 289 | for ( var j in hallorder ) { 290 | var hall = hallorder[j]; 291 | 292 | boundingX = 0; 293 | boundingY = 0; 294 | 295 | for ( var i in objects[hall] ) { 296 | renders[objects[hall][i]['class']](objects[hall][i], true); 297 | } 298 | 299 | hallsizes[hall] = { 300 | 'x': offsetX, 301 | 'y': offsetY, 302 | 'w': boundingX, 303 | 'h': boundingY, 304 | }; 305 | 306 | if (maxX < offsetX + boundingX + padX) { 307 | maxX = offsetX + boundingX + padX; 308 | } 309 | 310 | // Look ahead and see where if we can squeeze the hall in below this one. 311 | var nj = parseInt(j) + 1; 312 | if (nj < hallorder.length) { 313 | var nextHall = hallorder[nj]; 314 | if ((hallsizes[hall].y + hallsizes[hall].h + padY + hallsizes[nextHall].h) < maxHallHeight) { 315 | offsetY += boundingY + padY; 316 | } else { 317 | offsetY = 0; 318 | offsetX = maxX; 319 | } 320 | } 321 | } 322 | 323 | // Fourth phase: draw bounding boxes of halls. 324 | var halls = Object.keys(objects).length; 325 | var hallidx = 0; 326 | for ( var i in hallorder ) { 327 | var hall = hallorder[i]; 328 | var hue = (hallidx/halls) * 360; 329 | 330 | var size = hallsizes[hall]; 331 | 332 | var hallRect = paper.rect( 333 | (size.x - 15) * scaling, 334 | (size.y - 15) * scaling, 335 | (size.w + 30) * scaling, 336 | (size.h + 30) * scaling); 337 | var labelOffsetX = (size.w + 30) * scaling / 2; 338 | var labelOffsetY = -100; 339 | hallRect.attr({fill: 'hsla(' + hue + ',100%,50%,0.3)'}); 340 | hallRect.label = paper.text(hallRect.attr('x') + labelOffsetX, hallRect.attr('y') + labelOffsetY, hall); 341 | hallRect.label.attr({'font-size': 144}); 342 | hallRect.label.attr({'border': '1px solid red'}); 343 | hallRect.label.attr({'fill': 'rgba(200, 200, 200, 0.7)'}); 344 | 345 | hallidx++; 346 | } 347 | 348 | // Fifth phase: draw objects. 349 | for ( var i in hallorder ) { 350 | var hall = hallorder[i]; 351 | offsetX = hallsizes[hall].x; 352 | offsetY = hallsizes[hall].y; 353 | 354 | // Re-do with real paint this time 355 | boundingX = 0; 356 | boundingY = 0; 357 | for ( var i in objects[hall] ) { 358 | renders[objects[hall][i]['class']](objects[hall][i], false); 359 | } 360 | } 361 | } 362 | 363 | })(); 364 | -------------------------------------------------------------------------------- /src/dhmenu.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var dhmenu = {}; 4 | dhmenu.onclick = null; 5 | 6 | dhmenu.init = function(objects, click_callback) { 7 | dhmenu.onclick = click_callback; 8 | dhmenu.write(objects); 9 | } 10 | 11 | dhmenu.hideShowMenu = function(){ 12 | if($("#menu_container").is(":visible")){ 13 | $("#menu_container").hide(); 14 | } else { 15 | $("#menu_container").show(); 16 | } 17 | } 18 | 19 | // Write menu 20 | dhmenu.write = function(objects){ 21 | 22 | // Move dist and prod to end 23 | if(objects["Dist"]){ 24 | var dist = objects["Dist"]; 25 | delete objects["Dist"]; 26 | objects["Dist"] = dist; 27 | } 28 | if(objects["Prod"]){ 29 | var prod = objects["Prod"]; 30 | delete objects["Prod"]; 31 | objects["Prod"] = prod; 32 | } 33 | 34 | if($('#menu')){ 35 | var div_menu = $('#menu'); 36 | div_menu.empty(); 37 | 38 | // Add ul - Halls 39 | var ul_halls = $('
Interface:' 280 | + ifacename + '
Status:' + entry.status + '
Administratively Disabled
Spanning Tree:' + entry.stp + '
Speed:' + entry.speed + ' Mbit/s
Traffic (10 min avg.):' + 297 | 'TX: ' + Math.ceil(entry.tx_10min*8/1000/1000) + ' Mbit/s, ' + 298 | 'RX: ' + Math.ceil(entry.rx_10min*8/1000/1000) + ' Mbit/s
Traffic (instant):' + 301 | 'TX: ' + Math.ceil(entry.tx*8/1000/1000) + ' Mbit/s, ' + 302 | 'RX: ' + Math.ceil(entry.rx*8/1000/1000) + ' Mbit/s
Errors:In: ' + Math.ceil(entry.errors_in*100)/100 + 306 | ', Out: ' + Math.ceil(entry.errors_out*100)/100 + '
","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("