luci-app-bmx7: convert to JS

migrate away from the old luci-app lua control system.

See: https://github.com/openwrt/luci/issues/7310

The lua app was relatively broken prior to conversion.

Strangely, bmx7-uci-config package is what installs the
init.d service and uci config.

Signed-off-by: Paul Donald <newtwen+github@gmail.com>
This commit is contained in:
Paul Donald
2026-02-23 22:03:39 +01:00
parent d2b850b69c
commit f578786152
24 changed files with 1198 additions and 1190 deletions

View File

@@ -4,7 +4,7 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for BMX7
LUCI_DEPENDS:=+luci-compat +luci-lib-json +luci-base +bmx7 +bmx7-json
LUCI_DEPENDS:=+luci-base +bmx7 +bmx7-uci-config +bmx7-json +bmx7-tun +bmx7-iwinfo
PKG_MAINTAINER:= Roger Pueyo <roger.pueyo@guifi.net> \
Pau Escrich <p4u@dabax.net>
PKG_LICENSE:=GPL-2.0-or-later

View File

@@ -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;
}

View File

@@ -0,0 +1,58 @@
.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{
z-index: 0;
}
.njg-close{
cursor: pointer;
position: absolute;
right: 10px;
top: 10px;
}
.njg-close:before { content: "\2716"; }
.njg-metadata{
z-index: 0;
}
.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;
}

View File

@@ -0,0 +1,568 @@
// version 0.1
(function () {
/**
* vanilla JS implementation of jQuery.extend()
*/
d3._extend = function(defaults, options) {
let 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) {
let n = element.node(),
nPos = n.getBoundingClientRect();
let 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 BarnesHut 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){
let 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",
"trans"+"late(" + 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) {
let nodesMap = {},
nodes = graph.nodes.slice(), // copy
links = graph.links.slice(), // copy
nodes_length = graph.nodes.length,
links_length = graph.links.length;
for(let i = 0; i < nodes_length; i++) {
// count how many links every node has
nodes[i].linkCount = 0;
nodesMap[nodes[i].id] = i;
}
for(let c = 0; c < links_length; c++) {
let 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) {
let overlay = d3.select(".njg-overlay"),
overlayInner = d3.select(".njg-overlay > .njg-inner"),
html = "<p><b>id</b>: " + n.id + "</p>";
if(n.label) { html += "<p><b>label</b>: " + n.label + "</p>"; }
if(n.properties) {
for(let key in n.properties) {
if(!n.properties.hasOwnProperty(key)) { continue; }
html += "<p><b>"+key.replace(/_/g, " ")+"</b>: " + n.properties[key] + "</p>";
}
}
if(n.linkCount) { html += "<p><b>links</b>: " + n.linkCount + "</p>"; }
if(n.local_addresses) {
html += "<p><b>local addresses</b>:<br />" + n.local_addresses.join('<br />') + "</p>";
}
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) {
let overlay = d3.select(".njg-overlay"),
overlayInner = d3.select(".njg-overlay > .njg-inner"),
html = "<p><b>source</b>: " + (l.source.label || l.source.id) + "</p>";
html += "<p><b>target</b>: " + (l.target.label || l.target.id) + "</p>";
html += "<p><b>cost</b>: " + l.cost + "</p>";
if(l.properties) {
for(let key in l.properties) {
if(!l.properties.hasOwnProperty(key)) { continue; }
html += "<p><b>"+ key.replace(/_/g, " ") +"</b>: " + l.properties[key] + "</p>";
}
}
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") {
let 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");
}
}
let 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);
};
let 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();
let link = panner.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", function (link) {
let 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) {
let 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);
let 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) {
let 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");
}
let attrs = ["protocol",
"version",
"revision",
"metric",
"router_id",
"topology_id"],
html = "";
if(graph.label) {
html += "<h3>" + graph.label + "</h3>";
}
for(let i in attrs) {
let attr = attrs[i];
if(graph[attr]) {
html += "<p><b>" + attr + "</b>: <span>" + graph[attr] + "</span></p>";
}
}
// Add nodes and links count
html += "<p><b>nodes</b>: <span>" + graph.nodes.length + "</span></p>";
html += "<p><b>links</b>: <span>" + graph.links.length + "</span></p>";
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 "trans"+"late(" + 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") {
let 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);
}
});
}
};
})();

View File

@@ -1,23 +1,23 @@
/*
Copyright © 2011 Pau Escrich <pau@dabax.net>
Contributors Lluis Esquerda <eskerda@gmail.com>
Copyright © 2011 Pau Escrich <pau@dabax.net>
Contributors Lluis Esquerda <eskerda@gmail.com>
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 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.
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.
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".
The full GNU General Public License is included in this distribution in
the file called "COPYING".
*/
@@ -25,20 +25,20 @@
Table pooler is a function to easy call XHR poller.
new TablePooler(5,"/cgi-bin/bmx7-info", {'status':''}, "status_table", function(st){
var table = Array()
let table = Array()
table.push(st.first,st.second)
return table
}
The parameters are:
polling_time: time between pollings
json_url: the json url to fetch the data
json_call: the json call
output_table_id: the table where javascript will put the data
polling_time: time between polls
json_url: the JSON URL to fetch the data
json_call: the JSON call
output_table_id: the table where JavaScript will put the data
callback_function: the function that will be executed each polling_time
The callback_function must return an array of arrays (matrix).
In the code st is the data obtained from the json call
In the code st is the data obtained from the JSON call
*/
function TablePooler (time, jsonurl, getparams, div_id, callback) {
@@ -51,9 +51,9 @@ function TablePooler (time, jsonurl, getparams, div_id, callback) {
this.start = function(){
XHR.poll(this.time, this.jsonurl, this.getparams, function(x, st){
var data = this.callback(st);
var content;
for (var i = 0; i < data.length; i++){
let data = this.callback(st);
let content, rowDiv, rowId, cellDiv, cellId;
for (let i = 0; i < data.length; i++){
rowId = "trDiv_" + this.div_id + i;
rowDiv = document.getElementById(rowId);
if (rowDiv === null) {
@@ -62,7 +62,7 @@ function TablePooler (time, jsonurl, getparams, div_id, callback) {
rowDiv.className = "tr";
this.div.appendChild(rowDiv);
}
for (var j = 0; j < data[i].length; j++){
for (let j = 0; j < data[i].length; j++){
cellId = "tdDiv_" + this.div_id + i + j;
cellDiv = document.getElementById(cellId);
if (cellDiv === null) {

View File

@@ -0,0 +1,67 @@
'use strict';
'require view';
'require form';
'require tools.widgets as widgets';
return view.extend({
render() {
let m, s, o;
m = new form.Map('bmx7', _('BMX7'));
s = m.section(form.NamedSection, 'general');
s.anonymous = true;
o = s.option(form.Value, 'runtimeDir', _('runtimeDir'));
o = s.option(form.Value, 'trustedNodesDir', _('trustedNodesDir'));
s = m.section(form.TypedSection, 'plugin', _('Plugins'));
s.addremove = true;
s.anonymous = true;
o = s.option(form.Value, 'plugin', _('Plugin'));
s = m.section(form.TypedSection, 'dev', _('Devices'));
s.addremove = true;
s.anonymous = false;
o = s.option(widgets.DeviceSelect, 'dev', _('Dev'));
s = m.section(form.TypedSection, 'tunDev', _('Tunnel Devices'));
s.addremove = true;
s.anonymous = false;
o = s.option(form.Value, 'tunDev', _('Dev'));
o = s.option(form.Value, 'tun6Address', _('tun6Address'));
o = s.option(form.Value, 'tun4Address', _('tun4Address'));
s = m.section(form.TypedSection, 'tunOut', _('Gateway Devices'));
s.addremove = true;
s.anonymous = true;
o = s.option(form.Value, 'tunOut', _('tunOut'));
o.value('ip4');
o.value('ip6');
o = s.option(form.Value, 'network', _('Network'));
o.datatype = 'ipaddr';
o = s.option(form.Value, 'exportDistance', _('exportDistance'));
o.datatype = 'uinteger';
o = s.option(form.Value, 'minPrefixLen', _('minPrefixLen'));
o.datatype = 'uinteger';
s = m.section(form.NamedSection, 'luci', _('luci'));
s.uciconfig = 'bmx7-luci';
s.anonymous = true;
o = s.option(form.Flag, 'ignore', _('Ignore'));
o.default = '0';
o.rmempty = false;
o = s.option(form.Value, 'json', _('JSON source'));
o.rmempty = false;
return m.render();
},
});

View File

@@ -1,7 +0,0 @@
config 'bmx7' 'luci'
option ignore '0'
option place 'admin network BMX7'
#option place 'qmp Mesh'
option position '3'
#option json 'http://127.0.0.1/cgi-bin/bmx7-info?'
option json 'exec:/www/cgi-bin/bmx7-info -s'

View File

@@ -1,101 +0,0 @@
--[[
Copyright (C) 2011 Pau Escrich <pau@dabax.net>
Contributors Jo-Philipp Wich <xm@subsignal.org>
Roger Pueyo Centelles <roger.pueyo@guifi.net>
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".
--]]
module("luci.controller.bmx7", package.seeall)
function index()
local place = {}
local ucim = require "luci.model.uci"
local uci = ucim.cursor()
-- checking if ignore is on
if uci:get("luci-bmx7","luci","ignore") == "1" then
return nil
end
-- getting value from uci database
local uci_place = uci:get("luci-bmx7","luci","place")
-- default values
if uci_place == nil then
place = {"bmx7"}
else
local util = require "luci.util"
place = util.split(uci_place," ")
end
-- getting position of menu
local uci_position = uci:get("luci-bmx7","luci","position")
---------------------------
-- Placing the pages in the menu
---------------------------
-- Status (default)
entry(place,call("action_status_j"),place[#place],tonumber(uci_position))
table.insert(place,"Status")
entry(place,call("action_status_j"),"Status",0)
table.remove(place)
-- 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",2)
table.remove(place)
-- Tunnels
table.insert(place,"Gateways")
entry(place,call("action_tunnels_j"),"Gateways",3)
table.remove(place)
-- Integrate bmx7-mdns if present
if nixio.fs.stat("/usr/lib/lua/luci/model/cbi/bmx7-mdns.lua","type") ~= nil then
table.insert(place,"mDNS")
entry(place, cbi("bmx7-mdns"), "mesh DNS", 1).dependent=false
table.remove(place)
end
end
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()
luci.template.render("bmx7/nodes_j", {})
end

View File

@@ -1,40 +0,0 @@
<div class="cbi-map">
<div class="cbi-section">
<legend><%:Bmx7 mesh nodes%></legend>
<div class="cbi-section-node">
<div class="table" id="nodes_div">
<div class="tr table-titles">
<div class="th"><%:Name%></div>
<div class="th"><%:Short ID%></div>
<div class="th"><%:S/s/T/t%></div>
<div class="th"><%:Primary IPv6%></div>
<div class="th"><%:Via Neighbour%></div>
<div class="th"><%:Device%></div>
<div class="th"><%:Metric%></div>
<div class="th"><%:Last Ref%></div>
</div>
</div>
</div>
</div>
</div>
<script src="<%=resource%>/bmx7/js/polling.js"></script>
<script>
new TablePooler(10,"/cgi-bin/bmx7-info", {'originators':''}, "nodes_div", function(st){
var originators = st.originators;
var res = Array();
originators.forEach(function(originator,i){
var name = originator.name;
var shortId = originator.shortId;
var SsTt = originator.S+'/'+originator.s+'/'+originator.T+'/'+originator.t;
var primaryIp = originator.primaryIp;
var nbName = originator.nbName;
var dev = originator.dev;
var metric = originator.metric;
var lastRef = originator.lastRef;
res.push([name, shortId, SsTt, primaryIp,
nbName, dev, metric, lastRef]);
});
return res;
});
</script>

View File

@@ -1,130 +0,0 @@
<%+header%>
<script src="<%=resource%>/cbi.js"></script>
<script src="<%=resource%>/bmx7/js/polling.js"></script>
<div class="cbi-map">
<center>
<img src="<%=resource%>/bmx7/bmx7logo.png" />
<br />
<br />
A mesh routing protocol for Linux devices.<br />
Visit <a href="http://bmx6.net">bmx6.net</a> for more information.<br />
<br />
</center>
<div class="cbi-map-descr"></div>
<div class="cbi-section">
<legend><%:Node configuration%></legend>
<div class="cbi-section-node">
<div class="table" id="config_div">
<div class="tr table-titles">
<div class="th"><%:Short ID%></div>
<div class="th"><%:Node name%></div>
<div class="th"><%:Primary IPv6 address%></div>
<div class="th"><%:Node key%></div>
<div class="th"><%:Short DHash%></div>
<div class="th"><%:BMX7 revision%></div>
</div>
</div>
</div>
</div>
<div class="cbi-section">
<legend><%:Node status%></legend>
<div class="cbi-section-node">
<div class="table" id="status_div">
<div class="tr table-titles">
<div class="th"><%:Nodes seen%></div>
<div class="th"><%:Neighbours%></div>
<div class="th"><%:Tunnelled IPv6 address%></div>
<div class="th"><%:Tunnelled IPv4 address%></div>
<div class="th"><%:Uptime%></div>
<div class="th"><%:CPU usage%></div>
<div class="th"><%:Memory usage%></div>
<div class="th"><%:Tx queue%></div>
</div>
</div>
</div>
</div>
<div class="cbi-section">
<legend><%:Network interfaces%></legend>
<div class="cbi-section-node">
<div class="table" id="ifaces_div">
<div class="tr table-titles">
<div class="th"><%:Interface%></div>
<div class="th"><%:State%></div>
<div class="th"><%:Type%></div>
<div class="th"><%:Max rate%></div>
<div class="th"><%:Link-local IPv6%></div>
<div class="th"><%:RX BpP%></div>
<div class="th"><%:TX BpP%></div>
</div>
</div>
</div>
</div>
<div class="cbi-section">
<legend><%:Links%></legend>
<div class="cbi-section-node">
<div class="table" id="links_div">
<div class="tr table-titles">
<div class="th"><%:Short ID%></div>
<div class="th"><%:Name%></div>
<div class="th"><%:Link key%></div>
<div class="th"><%:Remote link-local IPv6%></div>
<div class="th"><%:Device%></div>
<div class="th"><%:RX rate%></div>
<div class="th"><%:TX rate%></div>
<div class="th"><%:Routes%></div>
</div>
</div>
</div>
</div>
</div>
<script>
new TablePooler(10,"/cgi-bin/bmx7-info", {'info':''}, "config_div", function(st){
var res = Array();
var sta = st.info[0].status;
res.push([sta.shortId, sta.name, sta.primaryIp, sta.nodeKey, sta.shortDhash, sta.revision]);
return res;
});
new TablePooler(10,"/cgi-bin/bmx7-info", {'info':''}, "status_div", function(st){
var res = Array();
var sta = st.info[0].status;
var mem = st.info[3].memory.bmx7;
var txQ = sta.txQ.split('/');
var ptxQ = '<p style="color:rgb('+parseInt(255*txQ[0]/txQ[1])+','+parseInt(128*(txQ[1]-txQ[0])/txQ[1])+',0)")>'+sta.txQ+'</p>';
res.push([sta.nodes, sta.nbs, sta.tun6Address, sta.tun4Address, sta.uptime, sta.cpu, mem, ptxQ]);
return res;
});
new TablePooler(10,"/cgi-bin/bmx7-info", {'info':''}, "ifaces_div", function(st){
var res = Array();
var ifaces = st.info[1].interfaces;
ifaces.forEach(function(iface){
res.push([iface.dev, iface.state, iface.type, iface.rateMax, iface.localIp, iface.rxBpP, iface.txBpP]);
});
return res;
});
new TablePooler(10,"/cgi-bin/bmx7-info", {'info':''}, "links_div", function(st){
var res = Array();
links = st.info[2].links;
links.forEach(function(link){
res.push([link.shortId, link.name, link.linkKey, link.nbLocalIp, link.dev, link.rxRate, link.txRate, link.rts]);
});
return res;
});
</script>
<%+footer%>

View File

@@ -1,54 +0,0 @@
<%+header%>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min.js" integrity="sha512-uy3foVtL4u0+5430l7zZt4PHjVtICfrbu3mtzdanR425sKD7kS5264djeZAzNIV0l4vc1QkFpW2+G5i5KoJIFQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="<%=resource%>/bmx7/js/netjsongraph.js"></script>
<link href="<%=resource%>/bmx7/css/netjsongraph.css" rel="stylesheet">
<style>
body {
font-family: Arial, sans-serif;
font-size: 13px;
}
.njg-overlay{
width: auto;
height: auto;
min-width: 200px;
max-width: 400px;
border: 1px solid #000;
border-radius: 2px;
background: rgba(0, 0, 0, 0.7);
top: 10px;
right: 10px;
padding: 0 15px;
font-family: Arial, sans-serif;
font-size: 14px;
color: #fff
}
.njg-node {
fill: #008000;
fill-opacity: 0.8;
stroke: #008000;
stroke-width: 1px;
cursor: pointer;
}
.njg-node:hover,
.njg-node.njg-open{
fill-opacity: 1;
}
.njg-link {
stroke: #00ff00;
stroke-width: 2;
stroke-opacity: .5;
cursor: pointer;
}
.njg-link:hover,
.njg-link.njg-open{
stroke-width: 3;
stroke-opacity: 1
}
</style>
<script>d3.netJsonGraph("/cgi-bin/bmx7-info?netjson/network-graph.json", { defaultStyle: false });</script>
<%+footer%>

View File

@@ -0,0 +1,73 @@
{
"admin/network/bmx7": {
"title": "BMX7",
"order": 50,
"action": {
"type": "view",
"path": "bmx7/config"
},
"depends": {
"acl": [ "luci-app-bmx7" ]
}
},
"admin/network/bmx7/config": {
"title": "Config",
"order": 1,
"action": {
"type": "view",
"path": "bmx7/config"
},
"depends": {
"acl": [ "luci-app-bmx7" ]
}
},
"admin/network/bmx7/status": {
"title": "Status",
"order": 3,
"action": {
"type": "template",
"path": "bmx7/bmxstatus"
},
"depends": {
"acl": [ "luci-app-bmx7" ]
}
},
"admin/network/bmx7/topology": {
"title": "Topology",
"order": 4,
"action": {
"type": "template",
"path": "bmx7/bmxtopology"
},
"depends": {
"acl": [ "luci-app-bmx7" ]
}
},
"admin/network/bmx7/nodes": {
"title": "Nodes",
"order": 5,
"action": {
"type": "template",
"path": "bmx7/bmxnodes"
},
"depends": {
"acl": [ "luci-app-bmx7" ]
}
},
"admin/network/bmx7/tunnels": {
"title": "Tunnels",
"order": 6,
"action": {
"type": "template",
"path": "bmx7/bmxtunnels"
},
"depends": {
"acl": [ "luci-app-bmx7" ]
}
}
}

View File

@@ -0,0 +1,18 @@
{
"luci-app-bmx7": {
"description": "Grant UCI access for luci-app-bmx7",
"read": {
"file": {
"/www/cgi-bin/bmx7-info *": "exec"
},
"uci": [
"bmx7"
]
},
"write": {
"uci": [
"bmx7"
]
}
}
}

View File

@@ -21,31 +21,38 @@
# the file called "COPYING".
#
# This script gives information about bmx7
# Can be executed from a linux shell: ./bmx7-info -s links
# Or from web interfae (with cgi enabled): http://host/cgi-bin/bmx7-info?links
# If you ask for a directory you wil get the directory contents in JSON forman
# Can be executed from a Linux shell: ./bmx7-info -s links
# Or from web interface (with cgi enabled): http://host/cgi-bin/bmx7-info?links
# If you ask for a directory you will get the directory contents in JSON format
BMX7_DIR="$(uci get bmx7.general.runtimeDir 2>/dev/null)" || BMX7_DIR="/var/run/bmx7/json"
#Checking if shell mode or cgi-bin mode
if [ "$1" == "-s" ]; then
QUERY="$2"
else
QUERY="${QUERY_STRING%%=*}"
echo "Content-type: application/json"
echo ""
fi
case "${1:-}" in
-s)
QUERY="$2"
;;
*)
QUERY="${QUERY_STRING%%&*}"
QUERY="${QUERY%%=*}"
printf 'Content-type: application/json\n\n'
;;
esac
check_path() {
[ -d "$1" ] && path=$(cd $1; pwd)
[ -f "$1" ] && path=$(cd $1/..; pwd)
[ $(echo "$path" | grep -c "^$BMX7_DIR") -ne 1 ] && exit 1
}
target="$1"
# Resolve real absolute path safely
resolved="$(cd "$(dirname -- "$target")" 2>/dev/null && pwd -P)/$(basename -- "$target")"
[ -e "$resolved" ] || return
}
print_mem() {
echo -n '{ "memory": { "bmx7": "'
cat /proc/$(cat /var/run/bmx7/pid)/status |grep -i VmSize | tr -s " " | cut -d " " -f 2,3 | tr -d "\n"
echo '"}}'
pid="$(pidof bmx7 2>/dev/null)" || return
[ -r "/proc/$pid/status" ] || return
vm=$(awk '/VmSize:/ {print $2" "$3}' "/proc/$pid/status")
printf '{ "memory": { "bmx7": "%s" }}' "$vm"
}
print_query() {
@@ -53,81 +60,75 @@ print_query() {
[ -d "$BMX7_DIR/$1" ] &&
{
# If /all has not been specified
[ -z "$QALL" ] &&
{
total=$(ls $BMX7_DIR/$1 | wc -w)
i=1
echo -n "{ \"$1\": [ "
for f in $(ls $BMX7_DIR/$1); do
echo -n "{ \"name\": \"$f\" }"
[ $i -lt $total ] && echo -n ','
i=$(( $i + 1 ))
if [ -z "$QALL" ]; then
first=1
printf '{ "%s": [ ' "$1"
for f in "$BMX7_DIR"/$1; do
[ -e "$f" ] || continue
printf '{ "name": "%s" }' "$(tr -d '\n' < "$f")"
[ $first -eq 0 ] && printf ','
first=0
done
echo -n " ] }"
printf " ] }"
# If /all has been specified, printing all the files together
} || {
comma=""
echo -n "[ "
for entry in "$BMX7_DIR/$1/"*; do
# If /all has been specified, print all the files together
else
first=1
printf "[ "
for entry in "$BMX7_DIR"/$1; do
[ -f "$entry" ] &&
{
${comma:+echo "$comma"}
tr -d '\n' < "$entry"
comma=","
[ $first -eq 0 ] && printf ','
printf "%s" "$(tr -d '\n' < "$entry")"
first=0
}
done
echo -n " ]"
}
printf " ]"
fi
}
# If the query is a file, just printing the file
# If the query is a file, just print the file
[ -f "$BMX7_DIR/$1" ] && [ -s "$BMX7_DIR/$1" ] && cat "$BMX7_DIR/$1" && return 0 || return 1
}
if [ "${QUERY##*/}" == "all" ]; then
if [ "${QUERY##*/}" = "all" ]; then
QUERY="${QUERY%/all}"
QALL=1
fi
if [ "$QUERY" == 'info' ]; then
echo '{ "info": [ '
print_query status
echo -n ","
print_query interfaces && echo -n "," || echo -n '{ "interfaces": "" },'
print_query links && echo -n "," || echo -n '{ "links": "" },'
print_mem
echo "] }"
fi
if [ "$QUERY" == 'neighbours' ]; then
QALL=1
echo '{ "neighbours": [ '
echo '{ "originators": '
print_query originators
echo '}, '
echo '{ "descriptions": '
print_query descriptions
echo "} ] }"
exit 0
else if [ "$QUERY" == 'tunnels' ]; then
bmx7 -c --jshow tunnels /r=0
exit 0
else if [ "$QUERY" == 'originators' ]; then
case "$QUERY" in
neighbours)
QALL=1
printf '{ "neighbours": [ '
printf '{ "originators": '
print_query originators
printf '}, '
printf '{ "descriptions": '
print_query descriptions
printf "} ] }"
exit 0
;;
tunnels)
bmx7 -c --jshow tunnels /r=0
exit 0
;;
originators)
bmx7 -c --jshow originators /r=0
exit 0
else
;;
info)
printf '{ "info": [ '
print_query status && printf "," || printf '{ "status": "" },'
print_query interfaces && printf "," || printf '{ "interfaces": "" },'
print_query links && printf "," || printf '{ "links": "" },'
print_mem
printf "] }"
;;
*)
check_path "$BMX7_DIR/$QUERY"
print_query $QUERY
exit 0
fi
fi
fi
fi
print_query "$QUERY"
;;
esac
ls -1F "$BMX7_DIR"
exit 0

View File

@@ -1,59 +0,0 @@
.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;
}

View File

@@ -1,62 +0,0 @@
.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;
}

View File

@@ -1,568 +0,0 @@
// 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 BarnesHut 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",
"trans"+"late(" + 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 = "<p><b>id</b>: " + n.id + "</p>";
if(n.label) { html += "<p><b>label</b>: " + n.label + "</p>"; }
if(n.properties) {
for(var key in n.properties) {
if(!n.properties.hasOwnProperty(key)) { continue; }
html += "<p><b>"+key.replace(/_/g, " ")+"</b>: " + n.properties[key] + "</p>";
}
}
if(n.linkCount) { html += "<p><b>links</b>: " + n.linkCount + "</p>"; }
if(n.local_addresses) {
html += "<p><b>local addresses</b>:<br />" + n.local_addresses.join('<br />') + "</p>";
}
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 = "<p><b>source</b>: " + (l.source.label || l.source.id) + "</p>";
html += "<p><b>target</b>: " + (l.target.label || l.target.id) + "</p>";
html += "<p><b>cost</b>: " + l.cost + "</p>";
if(l.properties) {
for(var key in l.properties) {
if(!l.properties.hasOwnProperty(key)) { continue; }
html += "<p><b>"+ key.replace(/_/g, " ") +"</b>: " + l.properties[key] + "</p>";
}
}
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 += "<h3>" + graph.label + "</h3>";
}
for(var i in attrs) {
var attr = attrs[i];
if(graph[attr]) {
html += "<p><b>" + attr + "</b>: <span>" + graph[attr] + "</span></p>";
}
}
// Add nodes and links count
html += "<p><b>nodes</b>: <span>" + graph.nodes.length + "</span></p>";
html += "<p><b>links</b>: <span>" + graph.links.length + "</span></p>";
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 "trans"+"late(" + 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);
}
});
}
};
})();

View File

@@ -1,4 +1,4 @@
<%#
{#
Copyright © 2011 Pau Escrich <pau@dabax.net>
Contributors Lluis Esquerda <eskerda@gmail.com>
Roger Pueyo Centelles <roger.pueyo@guifi.net>
@@ -19,11 +19,11 @@
The full GNU General Public License is included in this distribution in
the file called "COPYING".
-%>
-#}
<%+header%>
<script src="<%=resource%>/cbi.js"></script>
<script src="<%=resource%>/bmx7/js/polling.js"></script>
{% include('header', { }) %}
<script src="{{ resource }}/bmx7/js/polling.js"></script>
<style>
@@ -61,26 +61,26 @@
<div id="extra-info" class="info">
<br />
<center>
Tip: click the <img src="<%=resource%>/bmx7/world.png" /> icon to see individual node information.
Tip: click the <img src="{{ resource }}/bmx7/world.png" /> icon to see individual node information.
</center>
</div>
<div class="cbi-section">
<legend><%:Originators%></legend>
<legend>{{_('Originators')}}</legend>
<div class="cbi-section-node">
<div class="table" id="nodes_div">
<div class="tr table-titles">
<div class="th"></div>
<div class="th"><%:Name%></div>
<div class="th"><%:Short ID%></div>
<div class="th"><%:S/s/T/t%></div>
<div class="th"><%:Primary IPv6%></div>
<div class="th"><%:Via Neighbour%></div>
<div class="th"><%:Metric%></div>
<div class="th"><%:Last Desc%></div>
<div class="th"><%:Last Ref%></div>
<div class="th"><%: %></div>
<div class="th">{{_('Name')}}</div>
<div class="th">{{_('Short ID')}}</div>
<div class="th">S/s/T/t</div>
<div class="th">{{_('Primary IPv6')}}</div>
<div class="th">{{_('Via Neighbour')}}</div>
<div class="th">{{_('Metric')}}</div>
<div class="th">{{_('Last Desc')}}</div>
<div class="th">{{_('Last Ref')}}</div>
<div class="th">: </div>
</div>
</div>
</div>
@@ -89,31 +89,31 @@
</div>
<script>
var displayExtraInfo = function ( id ) {
let displayExtraInfo = function ( id ) {
document.getElementById('extra-info').innerHTML = document.getElementById(id).innerHTML;
}
new TablePooler(5,"/cgi-bin/bmx7-info", {'originators':''}, "nodes_div", function(st){
var infoicon = "<%=resource%>/bmx7/world_small.png";
var originators = st.originators;
var res = Array();
originators.forEach(function(originator,i){
var name = originator.name;
var shortId = originator.shortId;
var nodeId = originator.nodeId;
var extensions = originator.name;
var SsTt = originator.S+'/'+originator.s+'/'+originator.T+'/'+originator.t;
var nodeKey = originator.nodeKey;
var descSize = originator.descSize;
var primaryIp = originator.primaryIp;
var nbName = originator.nbName;
var dev = originator.dev;
var nbLocalIp = originator.nbLocalIp;
var metric = originator.metric;
var lastDesc = originator.lastDesc;
var lastRef = originator.lastRef;
new TablePooler(5,"/cgi-bin/bmx7-info", {'originators':'1'}, "nodes_div", function(st){
let infoicon = "{{ resource }}/bmx7/world_small.png";
let originators = st?.originators;
let res = Array();
originators.forEach(function(originator, i){
let name = originator?.name;
let shortId = originator?.shortId;
let nodeId = originator?.nodeId;
let extensions = originator?.name;
let SsTt = originator?.S+'/'+originator?.s+'/'+originator?.T+'/'+originator?.t;
let nodeKey = originator?.nodeKey;
let descSize = originator?.descSize;
let primaryIp = originator?.primaryIp;
let nbName = originator?.nbName;
let dev = originator?.dev;
let nbLocalIp = originator?.nbLocalIp;
let metric = originator?.metric;
let lastDesc = originator?.lastDesc;
let lastRef = originator?.lastRef;
var extrainfo = '<a onclick="displayExtraInfo(\'ip-' + i + '\')"><img src="' + infoicon + '" / ></a>';
var extrainfo_link = '<a onclick="displayExtraInfo(\'ip-' + i + '\')">' + '<img src="' + infoicon + '" />' + '</a>';
let extrainfo = '<a onclick="displayExtraInfo(\'ip-' + i + '\')"><img src="' + infoicon + '" / ></a>';
let extrainfo_link = '<a onclick="displayExtraInfo(\'ip-' + i + '\')">' + '<img src="' + infoicon + '" />' + '</a>';
extrainfo = '<div id="ip-'+ i +'" class="hideme">'
+ "<div class='inforow'>"
@@ -152,4 +152,4 @@
});
</script>
<%+footer%>
{% include('footer', { }) %}

View File

@@ -0,0 +1,132 @@
{% include('header', { }) %}
<script src="{{ resource }}/bmx7/js/polling.js"></script>
<div class="cbi-map">
<center>
<img src="{{ resource }}/bmx7/bmx7logo.png" />
<br />
<br />
A mesh routing protocol for Linux devices.<br />
Visit <a href="http://bmx6.net">bmx6.net</a> for more information.<br />
<br />
</center>
<div class="cbi-map-descr"></div>
<div class="cbi-section">
<legend>{{ _('Node configuration') }}</legend>
<div class="cbi-section-node">
<div class="table" id="config_div">
<div class="tr table-titles">
<div class="th">{{ _('Short ID') }}</div>
<div class="th">{{ _('Node name') }}</div>
<div class="th">{{ _('Primary IPv6 address') }}</div>
<div class="th">{{ _('Node key') }}</div>
<div class="th">{{ _('Short DHash') }}</div>
<div class="th">{{ _('BMX7 revision') }}</div>
</div>
</div>
</div>
</div>
<div class="cbi-section">
<legend>{{ _('Node status') }}</legend>
<div class="cbi-section-node">
<div class="table" id="status_div">
<div class="tr table-titles">
<div class="th">{{ _('Nodes seen') }}</div>
<div class="th">{{ _('Neighbours') }}</div>
<div class="th">{{ _('Tunnelled IPv6 address') }}</div>
<div class="th">{{ _('Tunnelled IPv4 address') }}</div>
<div class="th">{{ _('Uptime') }}</div>
<div class="th">{{ _('CPU usage') }}</div>
<div class="th">{{ _('Memory usage') }}</div>
<div class="th">{{ _('Tx queue') }}</div>
</div>
</div>
</div>
</div>
<div class="cbi-section">
<legend>{{ _('Network interfaces') }}</legend>
<div class="cbi-section-node">
<div class="table" id="ifaces_div">
<div class="tr table-titles">
<div class="th">{{ _('Interface') }}</div>
<div class="th">{{ _('State') }}</div>
<div class="th">{{ _('Type') }}</div>
<div class="th">{{ _('Max rate') }}</div>
<div class="th">{{ _('Link-local IPv6') }}</div>
<div class="th">{{ _('RX BpP') }}</div>
<div class="th">{{ _('TX BpP') }}</div>
</div>
</div>
</div>
</div>
<div class="cbi-section">
<legend>{{ _('Links') }}</legend>
<div class="cbi-section-node">
<div class="table" id="links_div">
<div class="tr table-titles">
<div class="th">{{ _('Short ID') }}</div>
<div class="th">{{ _('Name') }}</div>
<div class="th">{{ _('Link key') }}</div>
<div class="th">{{ _('Remote link-local IPv6') }}</div>
<div class="th">{{ _('Device') }}</div>
<div class="th">{{ _('RX rate') }}</div>
<div class="th">{{ _('TX rate') }}</div>
<div class="th">{{ _('Routes') }}</div>
</div>
</div>
</div>
</div>
</div>
<script>
new TablePooler(10,"/cgi-bin/bmx7-info", {'info':'1'}, "config_div", function(st){
let res = Array();
let sta = st?.info[0]?.status;
res.push([sta?.shortId, sta?.name, sta?.primaryIp, sta?.nodeKey, sta?.shortDhash, sta?.revision]);
return res;
});
new TablePooler(10,"/cgi-bin/bmx7-info", {'info':'1'}, "status_div", function(st){
let res = Array();
let sta = st?.info[0]?.status;
let mem = st?.info[3]?.memory?.bmx7;
let txQ = sta?.txQ?.split('/');
let ptxQ = '<p style="color:rgb('+parseInt(255*txQ?.[0]/txQ?.[1])+','+parseInt(128*(txQ?.[1]-txQ?.[0])/txQ?.[1])+',0)")>'+sta.txQ+'</p>';
res.push([sta.nodes, sta.nbs, sta.tun6Address, sta.tun4Address, sta.uptime, sta.cpu, mem, ptxQ]);
return res;
});
new TablePooler(10,"/cgi-bin/bmx7-info", {'info':'1'}, "ifaces_div", function(st){
let res = Array();
let ifaces = st?.info[1]?.interfaces;
if (ifaces)
ifaces?.forEach(function(iface) {
res.push([iface?.dev, iface?.state, iface?.type, iface?.rateMax, iface?.localIp, iface?.rxBpP, iface?.txBpP]);
});
return res;
});
new TablePooler(10,"/cgi-bin/bmx7-info", {'info':'1'}, "links_div", function(st){
let res = Array();
links = st?.info[2]?.links;
if (links)
links.forEach(function(link) {
res.push([link?.shortId, link?.name, link?.linkKey, link?.nbLocalIp, link?.dev, link?.rxRate, link?.txRate, link?.rts]);
});
return res;
});
</script>
{% include('footer', { }) %}

View File

@@ -0,0 +1,53 @@
{% include('header', { }) %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min.js" integrity="sha512-uy3foVtL4u0+5430l7zZt4PHjVtICfrbu3mtzdanR425sKD7kS5264djeZAzNIV0l4vc1QkFpW2+G5i5KoJIFQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="{{ resource }}/bmx7/js/netjsongraph.js"></script>
<link href="{{ resource }}/bmx7/css/netjsongraph.css" rel="stylesheet">
<style>
body {
font-family: Arial, sans-serif;
font-size: 13px;
}
.njg-overlay{
width: auto;
height: auto;
min-width: 200px;
max-width: 400px;
border: 1px solid #000;
border-radius: 2px;
background: rgba(0, 0, 0, 0.7);
top: 10px;
right: 10px;
padding: 0 15px;
font-family: Arial, sans-serif;
font-size: 14px;
color: #fff
}
.njg-node {
fill: #008000;
fill-opacity: 0.8;
stroke: #008000;
stroke-width: 1px;
cursor: pointer;
}
.njg-node:hover,
.njg-node.njg-open{
fill-opacity: 1;
}
.njg-link {
stroke: #00ff00;
stroke-width: 2;
stroke-opacity: .5;
cursor: pointer;
}
.njg-link:hover,
.njg-link.njg-open{
stroke-width: 3;
stroke-opacity: 1
}
</style>
<script>d3.netJsonGraph("/cgi-bin/bmx7-info?netjson/network-graph.json", { defaultStyle: false });</script>
{% include('footer', { }) %}

View File

@@ -1,4 +1,4 @@
<%#
{#
Copyright (C) 2011 Pau Escrich <pau@dabax.net>
Contributors Lluis Esquerda <eskerda@gmail.com>
@@ -18,31 +18,31 @@
The full GNU General Public License is included in this distribution in
the file called "COPYING".
-%>
-#}
<%+header%>
<script src="<%=resource%>/cbi.js"></script>
<script src="<%=resource%>/bmx7/js/polling.js"></script>
{% include('header', { }) %}
<script src="{{ resource }}/bmx7/js/polling.js"></script>
<div class="cbi-map">
<h2>Gateway announcements</h2>
<div class="cbi-map-descr">Networks announced by mesh nodes</div>
<div class="cbi-section">
<legend><%:Announcements%></legend>
<legend>{{ _('Announcements') }}</legend>
<div class="cbi-section-node">
<div class="table" id="tunnels_div">
<div class="tr table-titles">
<div class="th"><%:Status%></div>
<div class="th"><%:Name%></div>
<div class="th"><%:Node%></div>
<div class="th"><%:Network%></div>
<div class="th"><%:Bandwidth%></div>
<div class="th"><%:Local net%></div>
<div class="th"><%:Path Metric%></div>
<div class="th"><%:Tun Metric%></div>
<div class="th"><%:Rating%></div>
<div class="th">{{ _('Status') }}</div>
<div class="th">{{ _('Name') }}</div>
<div class="th">{{ _('Node') }}</div>
<div class="th">{{ _('Network') }}</div>
<div class="th">{{ _('Bandwidth') }}</div>
<div class="th">{{ _('Local net') }}</div>
<div class="th">{{ _('Path Metric') }}</div>
<div class="th">{{ _('Tun Metric') }}</div>
<div class="th">{{ _('Rating') }}</div>
</div>
</div>
</div>
@@ -51,26 +51,26 @@
</div>
<script>
new TablePooler(5,"/cgi-bin/bmx7-info", {'tunnels':''}, "tunnels_div", function(st){
var tunicon = "<%=resource%>/icons/tunnel.svg";
var tunicon_dis = "<%=resource%>/icons/tunnel_disabled.svg";
var applyicon = "<%=resource%>/cbi/apply.gif";
var res = Array();
for ( var k in st.tunnels ) {
var tunnel = st.tunnels[k];
var nodename = tunnel.remoteName;
var advnet = tunnel.advNet;
var status = '<img src="'+tunicon_dis+'"/>';
if ( tunnel.tunName != "---" ) status = '<img src="'+tunicon+'"/>';
new TablePooler(5,"/cgi-bin/bmx7-info", {'tunnels':'1'}, "tunnels_div", function(st){
let tunicon = "{{ resource }}/icons/tunnel.svg";
let tunicon_dis = "{{ resource }}/icons/tunnel_disabled.svg";
let applyicon = "{{ resource }}/cbi/apply.gif";
let res = Array();
for ( let k in st?.tunnels ) {
let tunnel = st?.tunnels[k];
let nodename = tunnel?.remoteName;
let advnet = tunnel?.advNet;
let status = '<img src="'+tunicon_dis+'"/>';
if ( tunnel?.tunName != "---" ) status = '<img src="'+tunicon+'"/>';
if ( advnet == "0.0.0.0/0" ) advnet = "<b>Internet IPv4</b>";
if ( advnet == "::/0" ) advnet = "<b>Internet IPv6</b>";
if (nodename != "---") {
res.push([status, tunnel.tunName, nodename, advnet, tunnel.advBw, tunnel.net,
tunnel.pathMtc, tunnel.tunMtc, tunnel.rating]);
res.push([status, tunnel?.tunName, nodename, advnet, tunnel?.advBw, tunnel?.net,
tunnel?.pathMtc, tunnel?.tunMtc, tunnel?.rating]);
}
}
return res;
});
</script>
<%+footer%>
{% include('footer', { }) %}