From a1e102093f7bf589d085e5315e6abd7f337d9ea6 Mon Sep 17 00:00:00 2001 From: p4u Date: Thu, 6 Dec 2018 12:17:49 +0100 Subject: [PATCH] luci-app-bmx7: refactory, multiple fixes and add topology graph Signed-off-by: p4u --- .../usr/lib/lua/luci/controller/bmx7.lua | 26 +- .../view/admin_status/index/bmx7_nodes.htm | 40 ++ .../usr/lib/lua/luci/view/bmx7/nodes_j.htm | 76 +-- .../usr/lib/lua/luci/view/bmx7/status_j.htm | 206 +++---- .../usr/lib/lua/luci/view/bmx7/topology.htm | 54 ++ .../usr/lib/lua/luci/view/bmx7/tunnels_j.htm | 76 +++ .../resources/bmx7/css/netjsongraph-theme.css | 59 ++ .../resources/bmx7/css/netjsongraph.css | 62 ++ .../resources/bmx7/js/netjsongraph.js | 568 ++++++++++++++++++ .../luci-static/resources/bmx7/js/polling.js | 40 +- 10 files changed, 1008 insertions(+), 199 deletions(-) create mode 100644 luci-app-bmx7/files/usr/lib/lua/luci/view/admin_status/index/bmx7_nodes.htm create mode 100644 luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/topology.htm create mode 100644 luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/tunnels_j.htm create mode 100644 luci-app-bmx7/files/www/luci-static/resources/bmx7/css/netjsongraph-theme.css create mode 100644 luci-app-bmx7/files/www/luci-static/resources/bmx7/css/netjsongraph.css create mode 100644 luci-app-bmx7/files/www/luci-static/resources/bmx7/js/netjsongraph.js diff --git a/luci-app-bmx7/files/usr/lib/lua/luci/controller/bmx7.lua b/luci-app-bmx7/files/usr/lib/lua/luci/controller/bmx7.lua index ed62628..a511dd9 100644 --- a/luci-app-bmx7/files/usr/lib/lua/luci/controller/bmx7.lua +++ b/luci-app-bmx7/files/usr/lib/lua/luci/controller/bmx7.lua @@ -59,9 +59,19 @@ function index() entry(place,call("action_status_j"),"Status",0) table.remove(place) - -- Nodes list + -- Topology + table.insert(place,"Topology") + entry(place,call("topology"),"Topology",1) + table.remove(place) + + -- Nodes table.insert(place,"Nodes") - entry(place,call("action_nodes_j"),"Nodes",1) + entry(place,call("action_nodes_j"),"Nodes",2) + table.remove(place) + + -- Tunnels + table.insert(place,"Gateways") + entry(place,call("action_tunnels_j"),"Gateways",3) table.remove(place) end @@ -70,8 +80,14 @@ function action_status_j() luci.template.render("bmx7/status_j", {}) end +function action_tunnels_j() + luci.template.render("bmx7/tunnels_j", {}) +end + +function topology() + luci.template.render("bmx7/topology", {}) +end + function action_nodes_j() - local http = require "luci.http" - local link_non_js = "/cgi-bin/luci" .. http.getenv("PATH_INFO") .. '/nodes_nojs' - luci.template.render("bmx7/nodes_j", {link_non_js=link_non_js}) + luci.template.render("bmx7/nodes_j", {}) end diff --git a/luci-app-bmx7/files/usr/lib/lua/luci/view/admin_status/index/bmx7_nodes.htm b/luci-app-bmx7/files/usr/lib/lua/luci/view/admin_status/index/bmx7_nodes.htm new file mode 100644 index 0000000..8a6aefa --- /dev/null +++ b/luci-app-bmx7/files/usr/lib/lua/luci/view/admin_status/index/bmx7_nodes.htm @@ -0,0 +1,40 @@ +
+
+ <%:Bmx7 mesh nodes%> +
+
+
+
<%:Name%>
+
<%:Short ID%>
+
<%:S/s/T/t%>
+
<%:Primary IPv6%>
+
<%:Via Neighbour%>
+
<%:Device%>
+
<%:Metric%>
+
<%:Last Ref%>
+
+
+
+
+
+ + + diff --git a/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/nodes_j.htm b/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/nodes_j.htm index 347af42..a631c93 100644 --- a/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/nodes_j.htm +++ b/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/nodes_j.htm @@ -27,19 +27,16 @@ +

Mesh nodes

@@ -70,40 +64,38 @@ Tip: click the icon to see individual node information.
-
+ + +
<%:Originators%> - - - - - - - - - - - - - - - - -
<%:Name%><%:Short ID%><%:S/s/T/t%><%:Primary IPv6 address%><%:Via neighbour%><%:Metric%><%:Last desc.%><%:Last ref.%><%: %>

<%:Collecting data...%>
-
+
+
+
+
+
<%:Name%>
+
<%:Short ID%>
+
<%:S/s/T/t%>
+
<%:Primary IPv6%>
+
<%:Via Neighbour%>
+
<%:Metric%>
+
<%:Last Desc%>
+
<%:Last Ref%>
+
<%: %>
+
+
+
+ <%+footer%> - diff --git a/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/status_j.htm b/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/status_j.htm index c6920f9..b7609d7 100644 --- a/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/status_j.htm +++ b/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/status_j.htm @@ -2,30 +2,6 @@ - -
@@ -38,145 +14,117 @@
-
+
<%:Node configuration%> - - - - - - - - - - - -
<%:Short ID%><%:Node name%><%:Primary IPv6 address%><%:Node key%><%:BMX7 revision%>

<%:Collecting data...%>
-
- -
+
+
+
+
<%:Short ID%>
+
<%:Node name%>
+
<%:Primary IPv6 address%>
+
<%:Node key%>
+
<%:Short DHash%>
+
<%:BMX7 revision%>
+
+
+
+
+ + +
<%:Node status%> - - - - - - - - - - - - - - - -
<%:Nodes seen%><%:Neighbours%><%:Tunnelled IPv6 address%><%:Tunnelled IPv4 address%><%:Uptime%><%:CPU usage%><%:Memory usage%><%:Tx queue%>

<%:Collecting data...%>
- - -
- <%:Interfaces%> - - - - - - - - - - - - - - -
<%:Interface%><%:State%><%:Type%><%:Max. rate%><%:Link-local IPv6 address%><%:Rx BpP%><%:Tx BpP%>

<%:Collecting data...%>
-
- -
- <%:Links%> - - - - - - - - - - - - - - - - -
+
+
+
+
<%:Nodes seen%>
+
<%:Neighbours%>
+
<%:Tunnelled IPv6 address%>
+
<%:Tunnelled IPv4 address%>
+
<%:Uptime%>
+
<%:CPU usage%>
+
<%:Memory usage%>
+
<%:Tx queue%>
+
+
+
+
+
+ <%:Network interfaces%> +
+
+
+
<%:Interface%>
+
<%:State%>
+
<%:Type%>
+
<%:Max rate%>
+
<%:LinkLocal Ipv6%>
+
<%:RX BpP%>
+
<%:TX BpP%>
+
+
+
+
+ + +
+ <%:Links%> +
+ +
+
<%+footer%> diff --git a/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/topology.htm b/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/topology.htm new file mode 100644 index 0000000..58ce9fd --- /dev/null +++ b/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/topology.htm @@ -0,0 +1,54 @@ +<%+header%> + + + + + + +<%+footer%> + diff --git a/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/tunnels_j.htm b/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/tunnels_j.htm new file mode 100644 index 0000000..aaa79a8 --- /dev/null +++ b/luci-app-bmx7/files/usr/lib/lua/luci/view/bmx7/tunnels_j.htm @@ -0,0 +1,76 @@ +<%# + Copyright (C) 2011 Pau Escrich + Contributors Lluis Esquerda + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + The full GNU General Public License is included in this distribution in + the file called "COPYING". +-%> + + +<%+header%> + + + +
+

Gateway announcements

+
Networks announced by mesh nodes
+ +
+ <%:Announcements%> +
+
+
+
<%:Status%>
+
<%:Name%>
+
<%:Node%>
+
<%:Network%>
+
<%:Bandwith%>
+
<%:Local net%>
+
<%:Path Metric%>
+
<%:Tun Metric%>
+
<%:Rating%>
+
+
+
+
+ +
+ + + +<%+footer%> diff --git a/luci-app-bmx7/files/www/luci-static/resources/bmx7/css/netjsongraph-theme.css b/luci-app-bmx7/files/www/luci-static/resources/bmx7/css/netjsongraph-theme.css new file mode 100644 index 0000000..276d362 --- /dev/null +++ b/luci-app-bmx7/files/www/luci-static/resources/bmx7/css/netjsongraph-theme.css @@ -0,0 +1,59 @@ +.njg-overlay{ + background: #fbfbfb; + border-radius: 2px; + border: 1px solid #ccc; + color: #6d6357; + font-family: Arial, sans-serif; + font-family: sans-serif; + font-size: 14px; + line-height: 20px; + height: auto; + max-width: 400px; + min-width: 200px; + padding: 0 15px; + right: 10px; + top: 10px; + width: auto; +} + +.njg-metadata{ + background: #fbfbfb; + border-radius: 2px; + border: 1px solid #ccc; + color: #6d6357; + display: none; + font-family: Arial, sans-serif; + font-family: sans-serif; + font-size: 14px; + height: auto; + left: 10px; + max-width: 500px; + min-width: 200px; + padding: 0 15px; + top: 10px; + width: auto; +} + +.njg-node{ + stroke-opacity: 0.5; + stroke-width: 7px; + stroke: #fff; +} + +.njg-node:hover, +.njg-node.njg-open { + stroke: rgba(0, 0, 0, 0.2); +} + +.njg-link{ + cursor: pointer; + stroke: #999; + stroke-width: 2; + stroke-opacity: 0.25; +} + +.njg-link:hover, +.njg-link.njg-open{ + stroke-width: 4 !important; + stroke-opacity: 0.5; +} diff --git a/luci-app-bmx7/files/www/luci-static/resources/bmx7/css/netjsongraph.css b/luci-app-bmx7/files/www/luci-static/resources/bmx7/css/netjsongraph.css new file mode 100644 index 0000000..556c520 --- /dev/null +++ b/luci-app-bmx7/files/www/luci-static/resources/bmx7/css/netjsongraph.css @@ -0,0 +1,62 @@ +.njg-hidden { + display: none !important; + visibility: hidden !important; +} + +.njg-tooltip{ + font-family: sans-serif; + font-size: 10px; + fill: #000; + opacity: 0.5; + text-anchor: middle; +} + +.njg-overlay{ + display: none; + position: absolute; + z-index: 11; +} + +.njg-close{ + cursor: pointer; + position: absolute; + right: 10px; + top: 10px; +} +.njg-close:before { content: "\2716"; } + +.njg-metadata{ + display: none; + position: absolute; + z-index: 12; +} + +.njg-node{ cursor: pointer } +.njg-link{ cursor: pointer } + +#njg-select-group { + text-align: center; + box-shadow: 0 0 10px #ccc; + position: fixed; + left: 50%; + top: 50%; + width: 50%; + margin-top: -7.5em; + margin-left: -25%; + padding: 5em 2em; +} + +#njg-select-group select { + font-size: 2em; + padding: 10px 15px; + width: 50%; + cursor: pointer; +} + +#njg-select-group option { + padding: 0.5em; +} + +#njg-select-group option[value=""] { + color: #aaa; +} diff --git a/luci-app-bmx7/files/www/luci-static/resources/bmx7/js/netjsongraph.js b/luci-app-bmx7/files/www/luci-static/resources/bmx7/js/netjsongraph.js new file mode 100644 index 0000000..66d0a5f --- /dev/null +++ b/luci-app-bmx7/files/www/luci-static/resources/bmx7/js/netjsongraph.js @@ -0,0 +1,568 @@ +// version 0.1 +(function () { + /** + * vanilla JS implementation of jQuery.extend() + */ + d3._extend = function(defaults, options) { + var extended = {}, + prop; + for(prop in defaults) { + if(Object.prototype.hasOwnProperty.call(defaults, prop)) { + extended[prop] = defaults[prop]; + } + } + for(prop in options) { + if(Object.prototype.hasOwnProperty.call(options, prop)) { + extended[prop] = options[prop]; + } + } + return extended; + }; + + /** + * @function + * @name d3._pxToNumber + * Convert strings like "10px" to 10 + * + * @param {string} val The value to convert + * @return {int} The converted integer + */ + d3._pxToNumber = function(val) { + return parseFloat(val.replace('px')); + }; + + /** + * @function + * @name d3._windowHeight + * + * Get window height + * + * @return {int} The window innerHeight + */ + d3._windowHeight = function() { + return window.innerHeight || document.documentElement.clientHeight || 600; + }; + + /** + * @function + * @name d3._getPosition + * + * Get the position of `element` relative to `container` + * + * @param {object} element + * @param {object} container + */ + d3._getPosition = function(element, container) { + var n = element.node(), + nPos = n.getBoundingClientRect(); + cPos = container.node().getBoundingClientRect(); + return { + top: nPos.top - cPos.top, + left: nPos.left - cPos.left, + width: nPos.width, + bottom: nPos.bottom - cPos.top, + height: nPos.height, + right: nPos.right - cPos.left + }; + }; + + /** + * netjsongraph.js main function + * + * @constructor + * @param {string} url The NetJSON file url + * @param {object} opts The object with parameters to override {@link d3.netJsonGraph.opts} + */ + d3.netJsonGraph = function(url, opts) { + /** + * Default options + * + * @param {string} el "body" The container element el: "body" [description] + * @param {bool} metadata true Display NetJSON metadata at startup? + * @param {bool} defaultStyle true Use css style? + * @param {bool} animationAtStart false Animate nodes or not on load + * @param {array} scaleExtent [0.25, 5] The zoom scale's allowed range. @see {@link https://github.com/mbostock/d3/wiki/Zoom-Behavior#scaleExtent} + * @param {int} charge -130 The charge strength to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#charge} + * @param {int} linkDistance 50 The target distance between linked nodes to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#linkDistance} + * @param {float} linkStrength 0.2 The strength (rigidity) of links to the specified value in the range. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#linkStrength} + * @param {float} friction 0.9 The friction coefficient to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#friction} + * @param {string} chargeDistance Infinity The maximum distance over which charge forces are applied. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#chargeDistance} + * @param {float} theta 0.8 The Barnes–Hut approximation criterion to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#theta} + * @param {float} gravity 0.1 The gravitational strength to the specified numerical value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#gravity} + * @param {int} circleRadius 8 The radius of circles (nodes) in pixel + * @param {string} labelDx "0" SVG dx (distance on x axis) attribute of node labels in graph + * @param {string} labelDy "-1.3em" SVG dy (distance on y axis) attribute of node labels in graph + * @param {function} onInit Callback function executed on initialization + * @param {function} onLoad Callback function executed after data has been loaded + * @param {function} onEnd Callback function executed when initial animation is complete + * @param {function} linkDistanceFunc By default high density areas have longer links + * @param {function} redraw Called when panning and zooming + * @param {function} prepareData Used to convert NetJSON NetworkGraph to the javascript data + * @param {function} onClickNode Called when a node is clicked + * @param {function} onClickLink Called when a link is clicked + */ + opts = d3._extend({ + el: "body", + metadata: true, + defaultStyle: true, + animationAtStart: true, + scaleExtent: [0.25, 5], + charge: -130, + linkDistance: 50, + linkStrength: 0.2, + friction: 0.9, // d3 default + chargeDistance: Infinity, // d3 default + theta: 0.8, // d3 default + gravity: 0.1, + circleRadius: 8, + labelDx: "0", + labelDy: "-1.3em", + nodeClassProperty: null, + linkClassProperty: null, + /** + * @function + * @name onInit + * + * Callback function executed on initialization + * @param {string|object} url The netJson remote url or object + * @param {object} opts The object of passed arguments + * @return {function} + */ + onInit: function(url, opts) {}, + /** + * @function + * @name onLoad + * + * Callback function executed after data has been loaded + * @param {string|object} url The netJson remote url or object + * @param {object} opts The object of passed arguments + * @return {function} + */ + onLoad: function(url, opts) {}, + /** + * @function + * @name onEnd + * + * Callback function executed when initial animation is complete + * @param {string|object} url The netJson remote url or object + * @param {object} opts The object of passed arguments + * @return {function} + */ + onEnd: function(url, opts) {}, + /** + * @function + * @name linkDistanceFunc + * + * By default, high density areas have longer links + */ + linkDistanceFunc: function(d){ + var val = opts.linkDistance; + if(d.source.linkCount >= 4 && d.target.linkCount >= 4) { + return val * 2; + } + return val; + }, + /** + * @function + * @name redraw + * + * Called on zoom and pan + */ + redraw: function() { + panner.attr("transform", + "translate(" + d3.event.translate + ") " + + "scale(" + d3.event.scale + ")" + ); + }, + /** + * @function + * @name prepareData + * + * Convert NetJSON NetworkGraph to the data structure consumed by d3 + * + * @param graph {object} + */ + prepareData: function(graph) { + var nodesMap = {}, + nodes = graph.nodes.slice(), // copy + links = graph.links.slice(), // copy + nodes_length = graph.nodes.length, + links_length = graph.links.length; + + for(var i = 0; i < nodes_length; i++) { + // count how many links every node has + nodes[i].linkCount = 0; + nodesMap[nodes[i].id] = i; + } + for(var c = 0; c < links_length; c++) { + var sourceIndex = nodesMap[links[c].source], + targetIndex = nodesMap[links[c].target]; + // ensure source and target exist + if(!nodes[sourceIndex]) { throw("source '" + links[c].source + "' not found"); } + if(!nodes[targetIndex]) { throw("target '" + links[c].target + "' not found"); } + links[c].source = nodesMap[links[c].source]; + links[c].target = nodesMap[links[c].target]; + // add link count to both ends + nodes[sourceIndex].linkCount++; + nodes[targetIndex].linkCount++; + } + return { "nodes": nodes, "links": links }; + }, + /** + * @function + * @name onClickNode + * + * Called when a node is clicked + */ + onClickNode: function(n) { + var overlay = d3.select(".njg-overlay"), + overlayInner = d3.select(".njg-overlay > .njg-inner"), + html = "

id: " + n.id + "

"; + if(n.label) { html += "

label: " + n.label + "

"; } + if(n.properties) { + for(var key in n.properties) { + if(!n.properties.hasOwnProperty(key)) { continue; } + html += "

"+key.replace(/_/g, " ")+": " + n.properties[key] + "

"; + } + } + if(n.linkCount) { html += "

links: " + n.linkCount + "

"; } + if(n.local_addresses) { + html += "

local addresses:
" + n.local_addresses.join('
') + "

"; + } + overlayInner.html(html); + overlay.classed("njg-hidden", false); + overlay.style("display", "block"); + // set "open" class to current node + removeOpenClass(); + d3.select(this).classed("njg-open", true); + }, + /** + * @function + * @name onClickLink + * + * Called when a node is clicked + */ + onClickLink: function(l) { + var overlay = d3.select(".njg-overlay"), + overlayInner = d3.select(".njg-overlay > .njg-inner"), + html = "

source: " + (l.source.label || l.source.id) + "

"; + html += "

target: " + (l.target.label || l.target.id) + "

"; + html += "

cost: " + l.cost + "

"; + if(l.properties) { + for(var key in l.properties) { + if(!l.properties.hasOwnProperty(key)) { continue; } + html += "

"+ key.replace(/_/g, " ") +": " + l.properties[key] + "

"; + } + } + overlayInner.html(html); + overlay.classed("njg-hidden", false); + overlay.style("display", "block"); + // set "open" class to current link + removeOpenClass(); + d3.select(this).classed("njg-open", true); + } + }, opts); + + // init callback + opts.onInit(url, opts); + + if(!opts.animationAtStart) { + opts.linkStrength = 2; + opts.friction = 0.3; + opts.gravity = 0; + } + if(opts.el == "body") { + var body = d3.select(opts.el), + rect = body.node().getBoundingClientRect(); + if (d3._pxToNumber(d3.select("body").style("height")) < 60) { + body.style("height", d3._windowHeight() - rect.top - rect.bottom + "px"); + } + } + var el = d3.select(opts.el).style("position", "relative"), + width = d3._pxToNumber(el.style('width')), + height = d3._pxToNumber(el.style('height')), + force = d3.layout.force() + .charge(opts.charge) + .linkStrength(opts.linkStrength) + .linkDistance(opts.linkDistanceFunc) + .friction(opts.friction) + .chargeDistance(opts.chargeDistance) + .theta(opts.theta) + .gravity(opts.gravity) + // width is easy to get, if height is 0 take the height of the body + .size([width, height]), + zoom = d3.behavior.zoom().scaleExtent(opts.scaleExtent), + // panner is the element that allows zooming and panning + panner = el.append("svg") + .attr("width", width) + .attr("height", height) + .call(zoom.on("zoom", opts.redraw)) + .append("g") + .style("position", "absolute"), + svg = d3.select(opts.el + " svg"), + drag = force.drag(), + overlay = d3.select(opts.el).append("div").attr("class", "njg-overlay"), + closeOverlay = overlay.append("a").attr("class", "njg-close"), + overlayInner = overlay.append("div").attr("class", "njg-inner"), + metadata = d3.select(opts.el).append("div").attr("class", "njg-metadata"), + metadataInner = metadata.append("div").attr("class", "njg-inner"), + closeMetadata = metadata.append("a").attr("class", "njg-close"), + // container of ungrouped networks + str = [], + selected = [], + /** + * @function + * @name removeOpenClass + * + * Remove open classes from nodes and links + */ + removeOpenClass = function () { + d3.selectAll("svg .njg-open").classed("njg-open", false); + }; + processJson = function(graph) { + /** + * Init netJsonGraph + */ + init = function(url, opts) { + d3.netJsonGraph(url, opts); + }; + /** + * Remove all instances + */ + destroy = function() { + force.stop(); + d3.select("#selectGroup").remove(); + d3.select(".njg-overlay").remove(); + d3.select(".njg-metadata").remove(); + overlay.remove(); + overlayInner.remove(); + metadata.remove(); + svg.remove(); + node.remove(); + link.remove(); + nodes = []; + links = []; + }; + /** + * Destroy and e-init all instances + * @return {[type]} [description] + */ + reInit = function() { + destroy(); + init(url, opts); + }; + + var data = opts.prepareData(graph), + links = data.links, + nodes = data.nodes; + + // disable some transitions while dragging + drag.on('dragstart', function(n){ + d3.event.sourceEvent.stopPropagation(); + zoom.on('zoom', null); + }) + // re-enable transitions when dragging stops + .on('dragend', function(n){ + zoom.on('zoom', opts.redraw); + }) + .on("drag", function(d) { + // avoid pan & drag conflict + d3.select(this).attr("x", d.x = d3.event.x).attr("y", d.y = d3.event.y); + }); + + force.nodes(nodes).links(links).start(); + + var link = panner.selectAll(".link") + .data(links) + .enter().append("line") + .attr("class", function (link) { + var baseClass = "njg-link", + addClass = null; + value = link.properties && link.properties[opts.linkClassProperty]; + if (opts.linkClassProperty && value) { + // if value is stirng use that as class + if (typeof(value) === "string") { + addClass = value; + } + else if (typeof(value) === "number") { + addClass = opts.linkClassProperty + value; + } + else if (value === true) { + addClass = opts.linkClassProperty; + } + return baseClass + " " + addClass; + } + return baseClass; + }) + .on("click", opts.onClickLink), + groups = panner.selectAll(".node") + .data(nodes) + .enter() + .append("g"); + node = groups.append("circle") + .attr("class", function (node) { + var baseClass = "njg-node", + addClass = null; + value = node.properties && node.properties[opts.nodeClassProperty]; + if (opts.nodeClassProperty && value) { + // if value is stirng use that as class + if (typeof(value) === "string") { + addClass = value; + } + else if (typeof(value) === "number") { + addClass = opts.nodeClassProperty + value; + } + else if (value === true) { + addClass = opts.nodeClassProperty; + } + return baseClass + " " + addClass; + } + return baseClass; + }) + .attr("r", opts.circleRadius) + .on("click", opts.onClickNode) + .call(drag); + + var labels = groups.append('text') + .text(function(n){ return n.label || n.id }) + .attr('dx', opts.labelDx) + .attr('dy', opts.labelDy) + .attr('class', 'njg-tooltip'); + + // Close overlay + closeOverlay.on("click", function() { + removeOpenClass(); + overlay.classed("njg-hidden", true); + }); + // Close Metadata panel + closeMetadata.on("click", function() { + // Reinitialize the page + if(graph.type === "NetworkCollection") { + reInit(); + } + else { + removeOpenClass(); + metadata.classed("njg-hidden", true); + } + }); + // default style + // TODO: probably change defaultStyle + // into something else + if(opts.defaultStyle) { + var colors = d3.scale.category20c(); + node.style({ + "fill": function(d){ return colors(d.linkCount); }, + "cursor": "pointer" + }); + } + // Metadata style + if(opts.metadata) { + metadata.attr("class", "njg-metadata").style("display", "block"); + } + + var attrs = ["protocol", + "version", + "revision", + "metric", + "router_id", + "topology_id"], + html = ""; + if(graph.label) { + html += "

" + graph.label + "

"; + } + for(var i in attrs) { + var attr = attrs[i]; + if(graph[attr]) { + html += "

" + attr + ": " + graph[attr] + "

"; + } + } + // Add nodes and links count + html += "

nodes: " + graph.nodes.length + "

"; + html += "

links: " + graph.links.length + "

"; + metadataInner.html(html); + metadata.classed("njg-hidden", false); + + // onLoad callback + opts.onLoad(url, opts); + + force.on("tick", function() { + link.attr("x1", function(d) { + return d.source.x; + }) + .attr("y1", function(d) { + return d.source.y; + }) + .attr("x2", function(d) { + return d.target.x; + }) + .attr("y2", function(d) { + return d.target.y; + }); + + node.attr("cx", function(d) { + return d.x; + }) + .attr("cy", function(d) { + return d.y; + }); + + labels.attr("transform", function(d) { + return "translate(" + d.x + "," + d.y + ")"; + }); + }) + .on("end", function(){ + force.stop(); + // onEnd callback + opts.onEnd(url, opts); + }); + + return force; + }; + + if(typeof(url) === "object") { + processJson(url); + } + else { + /** + * Parse the provided json file + * and call processJson() function + * + * @param {string} url The provided json file + * @param {function} error + */ + d3.json(url, function(error, graph) { + if(error) { throw error; } + /** + * Check if the json contains a NetworkCollection + */ + if(graph.type === "NetworkCollection") { + var selectGroup = body.append("div").attr("id", "njg-select-group"), + select = selectGroup.append("select") + .attr("id", "select"); + str = graph; + select.append("option") + .attr({ + "value": "", + "selected": "selected", + "name": "default", + "disabled": "disabled" + }) + .html("Choose the network to display"); + graph.collection.forEach(function(structure) { + select.append("option").attr("value", structure.type).html(structure.type); + // Collect each network json structure + selected[structure.type] = structure; + }); + select.on("change", function() { + selectGroup.attr("class", "njg-hidden"); + // Call selected json structure + processJson(selected[this.options[this.selectedIndex].value]); + }); + } + else { + processJson(graph); + } + }); + } + }; +})(); diff --git a/luci-app-bmx7/files/www/luci-static/resources/bmx7/js/polling.js b/luci-app-bmx7/files/www/luci-static/resources/bmx7/js/polling.js index 4a382eb..234391a 100644 --- a/luci-app-bmx7/files/www/luci-static/resources/bmx7/js/polling.js +++ b/luci-app-bmx7/files/www/luci-static/resources/bmx7/js/polling.js @@ -41,35 +41,41 @@ In the code st is the data obtained from the json call */ -function TablePooler (time, jsonurl, getparams, table_id, callback) { - this.table = document.getElementById(table_id); +function TablePooler (time, jsonurl, getparams, div_id, callback) { + this.div_id = div_id; + this.div = document.getElementById(div_id); this.callback = callback; this.jsonurl = jsonurl; this.getparams = getparams; this.time = time; - /* clear all rows */ - this.clear = function(){ - while( this.table.rows.length > 1 ) this.table.deleteRow(1); - } - this.start = function(){ XHR.poll(this.time, this.jsonurl, this.getparams, function(x, st){ var data = this.callback(st); - var content, tr, td; - this.clear(); + var content; for (var i = 0; i < data.length; i++){ - tr = this.table.insertRow(-1); - tr.className = 'cbi-section-table-row cbi-rowstyle-' + ((i % 2) + 1); - + rowId = "trDiv_" + this.div_id + i; + rowDiv = document.getElementById(rowId); + if (rowDiv === null) { + rowDiv = document.createElement("div"); + rowDiv.id = rowId; + rowDiv.className = "tr"; + this.div.appendChild(rowDiv); + } for (var j = 0; j < data[i].length; j++){ - td = tr.insertCell(-1); - if (data[i][j].length == 2) { - td.colSpan = data[i][j][1]; - content = data[i][j][0]; + cellId = "tdDiv_" + this.div_id + i + j; + cellDiv = document.getElementById(cellId); + if (cellDiv === null) { + cellDiv = document.createElement("div"); + cellDiv.id = cellId; + cellDiv.className = "td"; + rowDiv.appendChild(cellDiv); + } + if (typeof data[i][j] !== 'undefined' && data[i][j].length == 2) { + content = data[i][j][0] + "/" + data[i][j][1]; } else content = data[i][j]; - td.innerHTML = content; + cellDiv.innerHTML = content; } } }.bind(this)); -- 2.30.2