Compare commits

..

3 commits

Author SHA1 Message Date
Milan Pässler c66aeb2f8a Merge branch 'master' of https://github.com/plumpudding/hopglass into prometheus-charts 2016-04-01 00:53:19 +02:00
Milan Pässler e685fe385f Merge branch 'master' into prometheus-charts 2016-03-29 02:57:28 +02:00
Michael Rüttgers 2a58bcf5f1 infobox/node: render statistic charts based on prometheus backend
history:

Frontend generated node charts with graphite backend
Resolution for bower conflict
Let the user configure the metrics in config.json
Fix grunt task for deploying c3 css
Fix tooltip layout issue
2016-03-29 02:55:36 +02:00
45 changed files with 741 additions and 4325 deletions

View file

@ -1,16 +0,0 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
charset = utf-8
# Get rid of whitespace to avoid diffs with a bunch of EOL changes
trim_trailing_whitespace = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,html,scss,json}]
indent_size = 2
indent_style = space

View file

@ -1,7 +1,7 @@
language: node_js language: node_js
node_js: before_install:
- "lts/*" - gem install sass
- "node" - npm install -g grunt-cli
install: install:
- yarn - npm install
script: node_modules/.bin/grunt script: grunt

2
Gemfile Normal file
View file

@ -0,0 +1,2 @@
source 'https://rubygems.org'
gem "sass"

13
Gemfile.lock Normal file
View file

@ -0,0 +1,13 @@
GEM
remote: https://rubygems.org/
specs:
sass (3.4.16)
PLATFORMS
ruby
DEPENDENCIES
sass
BUNDLED WITH
1.10.6

View file

@ -17,7 +17,7 @@ module.exports = function (grunt) {
grunt.loadTasks("tasks") grunt.loadTasks("tasks")
grunt.registerTask("default", ["lint", "saveRevision", "copy", "sass", "postcss", "requirejs"]) grunt.registerTask("default", ["bower-install-simple", "lint", "saveRevision", "copy", "sass", "requirejs"])
grunt.registerTask("lint", ["eslint"]) grunt.registerTask("lint", ["eslint"])
grunt.registerTask("dev", ["default", "connect:server", "watch"]) grunt.registerTask("dev", ["default", "connect:server", "watch"])
} }

View file

@ -1,8 +1,8 @@
[![Build Status](https://travis-ci.org/hopglass/hopglass.svg?branch=master)](https://travis-ci.org/hopglass/hopglass) [![Build Status](https://travis-ci.org/plumpudding/hopglass.svg?branch=master)](https://travis-ci.org/plumpudding/hopglass)
# HopGlass # HopGlass
HopGlass is a frontend for the [HopGlass Server](https://github.com/hopglass/hopglass-server). HopGlass is a frontend for the [HopGlass Server](https://github.com/plumpudding/hopglass-server).
# Screenshots # Screenshots
@ -14,32 +14,31 @@ HopGlass is a frontend for the [HopGlass Server](https://github.com/hopglass/hop
# Dependencies # Dependencies
- NodeJS - npm
- yarn (recommended) or npm - bower
- grunt-cli
- Sass (>= 3.2)
# Installing dependencies # Installing dependencies
Install npm package-manager. On Debian-like systems run: Install npm and Sass with your package-manager. On Debian-like systems run:
sudo apt-get install nodejs sudo apt-get install npm ruby-sass
**Note:** The official Debian packages for NodeJS are quite old, you might want to check at [NodeSource](https://github.com/nodesource/distributions/blob/master/README.md) for current binaries installable with your distribution's package manager. or if you have bundler you can install ruby-sass simply via `bundle install`
On Mac you can install nodejs and yarn via brew: On Mac you have to install only npm via brew and sass
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install node brew install node
brew install yarn sudo gem install sass
On Arch Linux you can install nodejs and yarn via pacman:
sudo pacman -S nodejs yarn
Execute these commands on your server as a normal user to prepare the dependencies: Execute these commands on your server as a normal user to prepare the dependencies:
git clone https://github.com/hopglass/hopglass git clone https://github.com/plumpudding/hopglass
cd hopglass cd hopglass
yarn install # or `npm install` npm install
npm install grunt-cli
# Building # Building
@ -55,7 +54,7 @@ Copy `config.json.example` to `build/config.json` and change it to match your co
## dataPath (string/array) ## dataPath (string/array)
`dataPath` can be either a string containing the address of a [HopGlass Server](https://github.com/hopglass/hopglass-server) or an array containing multiple addresses. `dataPath` can be either a string containing the address of a [HopGlass Server](https://github.com/plumpudding/hopglass-server) or an array containing multiple addresses.
Don't forget the trailing slash! Don't forget the trailing slash!
Also, proxying the data through a webserver will allow GZip and thus will greatly reduce bandwidth consumption. Also, proxying the data through a webserver will allow GZip and thus will greatly reduce bandwidth consumption.
It may help with firewall problems too. It may help with firewall problems too.
@ -120,12 +119,12 @@ Examples for `nodeInfos`:
{ "name": "Clientstatistik", { "name": "Clientstatistik",
"href": "stats/dashboard/db/node-byid?var-nodeid={NODE_ID}", "href": "stats/dashboard/db/node-byid?var-nodeid={NODE_ID}",
"thumbnail": "stats/render/dashboard-solo/db/node-byid?panelId=1&fullscreen&theme=light&width=600&height=300&var-nodeid={NODE_ID}", "thumbnail": "stats/render/dashboard-solo/db/node-byid?panelId=1&fullscreen&theme=light&width=600&height=300&var-nodeid={NODE_ID}"
"caption": "Knoten {NODE_ID}" "caption": "Knoten {NODE_ID}"
}, },
{ "name": "Uptime", { "name": "Uptime",
"href": "stats/dashboard/db/node-byid?var-nodeid={NODE_ID}", "href": "stats/dashboard/db/node-byid?var-nodeid={NODE_ID}",
"thumbnail": "stats/render/dashboard-solo/db/node-byid?panelId=2&fullscreen&theme=light&width=600&height=300&var-nodeid={NODE_ID}", "thumbnail": "stats/render/dashboard-solo/db/node-byid?panelId=2&fullscreen&theme=light&width=600&height=300&var-nodeid={NODE_ID}"
"caption": "Knoten {NODE_ID}" "caption": "Knoten {NODE_ID}"
} }
] ]

27
app.js
View file

@ -1,20 +1,21 @@
require.config({ require.config({
baseUrl: "lib", baseUrl: "lib",
paths: { paths: {
"leaflet": "../node_modules/leaflet/dist/leaflet", "leaflet": "../bower_components/leaflet/dist/leaflet",
"leaflet.label": "../node_modules/leaflet-label/dist/leaflet.label", "leaflet.label": "../bower_components/Leaflet.label/dist/leaflet.label",
"leaflet.providers": "../node_modules/leaflet-providers/leaflet-providers", "leaflet.providers": "../bower_components/leaflet-providers/leaflet-providers",
"chroma-js": "../node_modules/chroma-js/chroma.min", "chroma-js": "../bower_components/chroma-js/chroma.min",
"moment": "../node_modules/moment/min/moment-with-locales.min", "moment": "../bower_components/moment/min/moment-with-locales.min",
"tablesort": "../node_modules/tablesort/tablesort.min", "tablesort": "../bower_components/tablesort/tablesort.min",
"tablesort.numeric": "../node_modules/tablesort/src/sorts/tablesort.numeric", "tablesort.numeric": "../bower_components/tablesort/src/sorts/tablesort.numeric",
"d3": "../node_modules/d3/d3.min", "d3": "../bower_components/d3/d3.min",
"numeral": "../node_modules/numeraljs/min/numeral.min", "numeral": "../bower_components/numeraljs/min/numeral.min",
"numeral-intl": "../node_modules/numeraljs/min/languages.min", "numeral-intl": "../bower_components/numeraljs/min/languages.min",
"virtual-dom": "../node_modules/virtual-dom/dist/virtual-dom", "virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
"rbush": "../node_modules/rbush/rbush", "rbush": "../bower_components/rbush/rbush",
"helper": "../helper", "helper": "../helper",
"jshashes": "../node_modules/jshashes/hashes" "jshashes": "../bower_components/jshashes/hashes",
"c3": "../bower_components/c3/c3.min"
}, },
shim: { shim: {
"leaflet.label": ["leaflet"], "leaflet.label": ["leaflet"],

40
bower.json Normal file
View file

@ -0,0 +1,40 @@
{
"name": "HopGlass",
"ignore": [
"node_modules",
"bower_components",
"**/.*",
"test",
"tests"
],
"dependencies": {
"Leaflet.label": "~0.2.1",
"chroma-js": "~0.6.1",
"leaflet": "~0.7.3",
"ionicons": "~2.0.1",
"moment": "~2.9.0",
"requirejs": "~2.1.16",
"tablesort": "https://github.com/tristen/tablesort.git#v3.0.2",
"roboto-slab-fontface": "*",
"es6-shim": "~0.27.1",
"almond": "~0.3.1",
"r.js": "~2.1.16",
"d3": "~3.5.5",
"numeraljs": "~1.5.3",
"roboto-fontface": "~0.3.0",
"virtual-dom": "~2.0.1",
"leaflet-providers": "~1.0.27",
"rbush": "https://github.com/mourner/rbush.git#~1.3.5",
"jshashes": "~1.0.5",
"c3": "~0.4.10"
},
"authors": [
"Milan Pässler <me@petabyteboy.de>",
"Nils Schneider <nils@nilsschneider.net>"
],
"license": "AGPL3",
"private": true,
"resolutions": {
"d3": "~3.5.5"
}
}

View file

@ -1,6 +1,6 @@
({ ({
baseUrl: "lib", baseUrl: "lib",
name: "../node_modules/almond/almond", name: "../bower_components/almond/almond",
mainConfigFile: "app.js", mainConfigFile: "app.js",
include: "../app", include: "../app",
wrap: true, wrap: true,

View file

@ -1,62 +1,62 @@
{ {
"dataPath": "https://map.ffdus.de/data/", "dataPath": [
"siteName": "Freifunk Flingern", "http://map.ffgl.eu/data/",
"http://karte.ffdus.de/data/",
"http://karte.freifunk-iserlohn.de/data/"
],
"siteName": "Freifunk Fluss",
"mapSigmaScale": 0.5, "mapSigmaScale": 0.5,
"showContact": true, "showContact": true,
"maxAge": 14, "maxAge": 14,
"mapLayers": [ "mapLayers": [
{ "name": "CartoDB", { "name": "MapQuest",
"url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png", "url": "https://otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.jpg",
"config": { "config": {
"maxZoom": 18, "subdomains": "1234",
"attribution": "&copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>, &copy; | <a href=\"https://carto.com/attribution\">CARTO</a>" "type": "osm",
"attribution": "Tiles &copy; <a href=\"https://www.mapquest.com/\" target=\"_blank\">MapQuest</a>, Data CC-BY-SA OpenStreetMap",
"maxZoom": 18
} }
}, },
{ {
"name": "OpenStreetMap.HOT" "name": "Stamen.TonerLite"
},
{
"name": "Luftbilder NRW",
"url": "https://www.wms.nrw.de/geobasis/wms_nw_dop?",
"config": {
"maxZoom": 20,
"attribution": "<a href=\"http://www.bezreg-koeln.nrw.de/brk_internet/geobasis/luftbilderzeugnisse/digitale_orthophotos/index.html\">DOP20</a>, Land NRW (2017), Datenlizenz Deutschland - Namensnennung - Version 2.0 (<a href=\"https://www.govdata.de/dl-de/by-2-0\">www.govdata.de/dl-de/by-2-0</a>)",
"format": "image/jpeg",
"layers": "nw_dop_rgb"
}
}
],
"nodeInfos": [
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/node-byid?var-nodeid={NODE_ID}",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/node-byid?panelId=1&theme=light&width=600&height=300&var-nodeid={NODE_ID}"
},
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/node-byid?var-nodeid={NODE_ID}",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/node-byid?panelId=2&theme=light&width=600&height=500&var-nodeid={NODE_ID}"
},
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/node-byid?var-nodeid={NODE_ID}",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/node-byid?panelId=3&theme=light&width=600&height=200&var-nodeid={NODE_ID}"
}
],
"globalInfos": [
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/global?var-job=dus",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/global?panelId=1&&theme=light&width=800&height=600&var-job=dus"
},
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/global?var-job=dus",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/global?panelId=8&&theme=light&width=800&height=600&var-job=dus"
}
],
"linkInfos": [
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/links?var-source={SOURCE}&var-target={TARGET}",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/links?panelId=1&&theme=light&width=800&height=600&var-source={SOURCE}&var-target={TARGET}"
} }
], ],
"siteNames": [ "siteNames": [
{ "site": "dus", "name": "Flingern" } { "site": "ffgl-bcd", "name": "Burscheid" },
{ "site": "ffgl-bgl", "name": "Bergisch Gladbach" },
{ "site": "ffgl-lln", "name": "Leichlingen" },
{ "site": "ffgl-ode", "name": "Odenthal" },
{ "site": "ffgl-ovr", "name": "Overath" },
{ "site": "ffgl-rrh", "name": "Rösrath" },
{ "site": "ffdus", "name": "Flingern" }
{ "site": "ffis", "name": "Iserlohn" }
], ],
"hwImg": [ "nodeCharts": [
{ "thumbnail": "https://cdn.rawgit.com/Moorviper/meshviewer_hwpics/master/nodes/{MODELHASH}.svg", {
"caption": "Knoten {MODELHASH}" "name": "Statistik",
"metrics": [
{
"id": "clients",
"color": "#1566A9",
"label": "Clients",
"query": "sum(statistics_clients_total{nodeid=%22{{NODE_ID}}%22})"
},
{
"id": "load",
"color": "1566A9",
"label": "Systemlast",
"query": "sum(statistics_loadavg{nodeid=%22{{NODE_ID}}%22})"
},
{
"id": "rx",
"color": "#1566A9",
"label": "Traffic (RX, kbps)",
"query": "sum(rate(statistics_traffic_rx_bytes{nodeid=%22{{NODE_ID}}%22}[30m])/125)"
}
],
"defaultMetric": "clients",
"url": "https://prometheus.map.eulenfunk.de/api/v1/query_range"
} }
] ]
} }

View file

@ -1,23 +1,23 @@
function get(url) { function get(url) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest() var req = new XMLHttpRequest();
req.open('GET', url) req.open('GET', url);
req.onload = function() { req.onload = function() {
if (req.status == 200) { if (req.status == 200) {
resolve(req.response) resolve(req.response);
} }
else { else {
reject(Error(req.statusText)) reject(Error(req.statusText));
}
} }
};
req.onerror = function() { req.onerror = function() {
reject(Error("Network Error")) reject(Error("Network Error"));
} };
req.send() req.send();
}) });
} }
function getJSON(url) { function getJSON(url) {
@ -75,8 +75,7 @@ function localStorageTest() {
function listReplace(s, subst) { function listReplace(s, subst) {
for (key in subst) { for (key in subst) {
var re = new RegExp(key, 'g') s = s.replace(key, subst[key])
s = s.replace(re, subst[key])
} }
return s return s
} }
@ -130,13 +129,7 @@ function attributeEntry(el, label, value) {
var tr = document.createElement("tr") var tr = document.createElement("tr")
var th = document.createElement("th") var th = document.createElement("th")
if (typeof label === "string")
th.textContent = label th.textContent = label
else {
th.appendChild(label)
tr.className = "routerpic"
}
tr.appendChild(th) tr.appendChild(th)
var td = document.createElement("td") var td = document.createElement("td")
@ -155,8 +148,8 @@ function attributeEntry(el, label, value) {
function createIframe(opt, width, height) { function createIframe(opt, width, height) {
el = document.createElement("iframe") el = document.createElement("iframe")
width = typeof width !== 'undefined' ? width : '100%' width = typeof width !== 'undefined' ? width : '525px';
height = typeof height !== 'undefined' ? height : '350px' height = typeof height !== 'undefined' ? width : '350px';
if (opt.src) if (opt.src)
el.src = opt.src el.src = opt.src
@ -186,7 +179,7 @@ function createIframe(opt, width, height) {
function showStat(o, subst) { function showStat(o, subst) {
var content, caption var content, caption
subst = typeof subst !== 'undefined' ? subst : {} subst = typeof subst !== 'undefined' ? subst : {};
if (o.thumbnail) { if (o.thumbnail) {
content = document.createElement("img") content = document.createElement("img")
@ -201,7 +194,7 @@ function showStat(o, subst) {
} }
if (o.iframe) { if (o.iframe) {
content = createIframe(o.iframe, o.width, o.height) content = createIframe(o.iframe)
if (o.iframe.src) if (o.iframe.src)
content.src = listReplace(o.iframe.src, subst) content.src = listReplace(o.iframe.src, subst)
else else

View file

@ -4,8 +4,9 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<link rel="stylesheet" href="css/ionicons.min.css"> <link rel="stylesheet" href="css/ionicons.min.css">
<link rel="stylesheet" href="css/roboto-slab/roboto-slab-fontface.css"> <link rel="stylesheet" href="roboto-slab-fontface.css">
<link rel="stylesheet" href="css/roboto/roboto-fontface.css"> <link rel="stylesheet" href="roboto-fontface.css">
<link rel="stylesheet" href="c3.min.css">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="vendor/es6-shim/es6-shim.min.js"></script> <script src="vendor/es6-shim/es6-shim.min.js"></script>
<script src="app.js"></script> <script src="app.js"></script>
@ -14,15 +15,5 @@
</script> </script>
</head> </head>
<body> <body>
<div class="loader">
<p>
Lade<br />
<span class="spinner"></span><br />
Karte &amp; Knoten...
</p>
<noscript>
<strong>JavaScript required</strong>
</noscript>
</div>
</body> </body>
</html> </html>

View file

@ -3,14 +3,15 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<link rel="stylesheet" href="node_modules/roboto-fontface/css/roboto-slab/roboto-slab-fontface.css"> <link rel="stylesheet" href="bower_components/roboto-slab-fontface/roboto-slab-fontface.css">
<link rel="stylesheet" href="node_modules/roboto-fontface/css/roboto/roboto-fontface.css"> <link rel="stylesheet" href="bower_components/roboto-fontface/roboto-fontface.css">
<link rel="stylesheet" href="node_modules/leaflet/dist/leaflet.css"> <link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="node_modules/leaflet-label/dist/leaflet.label.css"> <link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css">
<link rel="stylesheet" href="node_modules/ionicons/css/ionicons.min.css"> <link rel="stylesheet" href="bower_components/ionicons/css/ionicons.min.css">
<link rel="stylesheet" href="bower_components/c3/c3.min.css">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="node_modules/es6-shim/es6-shim.min.js"></script> <script src="bower_components/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/requirejs/require.js" data-main="app"></script> <script src="bower_components/requirejs/require.js" data-main="app"></script>
</head> </head>
<body> <body>
</body> </body>

View file

@ -29,8 +29,8 @@ define(function () {
s += "https://www.gnu.org/licenses/</a>.</p>" s += "https://www.gnu.org/licenses/</a>.</p>"
s += "<p>The source code is available at " s += "<p>The source code is available at "
s += "<a href=\"https://github.com/hopglass/hopglass\">" s += "<a href=\"https://github.com/plumpudding/hopglass\">"
s += "https://github.com/hopglass/hopglass</a>." s += "https://github.com/plumpudding/hopglass</a>."
el.innerHTML = s el.innerHTML = s
} }

View file

@ -4,8 +4,9 @@ define([], function () {
var n = Object.create(data) var n = Object.create(data)
n.nodes = {} n.nodes = {}
for (var key in data.nodes) for (var key in data.nodes) {
n.nodes[key] = data.nodes[key].filter(filter) n.nodes[key] = data.nodes[key].filter(filter)
}
var filteredIds = new Set() var filteredIds = new Set()

View file

@ -1,7 +1,7 @@
define(["d3"], function (d3) { define(["d3"], function (d3) {
var margin = 200 var margin = 200
var NODE_RADIUS = 15 var NODE_RADIUS = 15
var LINE_RADIUS = 7 var LINE_RADIUS = 12
return function (config, linkScale, sidebar, router) { return function (config, linkScale, sidebar, router) {
var self = this var self = this
@ -214,7 +214,7 @@ define(["d3"], function (d3) {
function drawLabel(d) { function drawLabel(d) {
var neighbours = d.neighbours.filter(function (d) { var neighbours = d.neighbours.filter(function (d) {
return !d.link.o.isVPN return d.link.o.type !== "VPN"
}) })
var sum = neighbours.reduce(function (a, b) { var sum = neighbours.reduce(function (a, b) {
@ -242,8 +242,7 @@ define(["d3"], function (d3) {
} }
function visibleLinks(d) { function visibleLinks(d) {
return (d.o.isVPN || return (d.source.x > screenRect.left && d.source.x < screenRect.right &&
d.source.x > screenRect.left && d.source.x < screenRect.right &&
d.source.y > screenRect.top && d.source.y < screenRect.bottom) || d.source.y > screenRect.top && d.source.y < screenRect.bottom) ||
(d.target.x > screenRect.left && d.target.x < screenRect.right && (d.target.x > screenRect.left && d.target.x < screenRect.right &&
d.target.y > screenRect.top && d.target.y < screenRect.bottom) d.target.y > screenRect.top && d.target.y < screenRect.bottom)
@ -325,19 +324,16 @@ define(["d3"], function (d3) {
links.forEach(function (d) { links.forEach(function (d) {
var dx = d.target.x - d.source.x var dx = d.target.x - d.source.x
var dy = d.target.y - d.source.y var dy = d.target.y - d.source.y
var a = Math.sqrt(dx * dx + dy * dy) * 2 var a = Math.sqrt(dx * dx + dy * dy)
dx /= a dx /= a
dy /= a dy /= a
var distancex = d.target.x - d.source.x - (10 * dx)
var distancey = d.target.y - d.source.y - (10 * dy)
ctx.beginPath() ctx.beginPath()
ctx.moveTo(d.source.x + dx * nodeRadius, d.source.y + dy * nodeRadius) ctx.moveTo(d.source.x + dx * nodeRadius, d.source.y + dy * nodeRadius)
ctx.lineTo(d.target.x - (distancex / 2) - dx * nodeRadius, d.target.y - (distancey / 2) - dy * nodeRadius) ctx.lineTo(d.target.x - dx * nodeRadius, d.target.y - dy * nodeRadius)
ctx.strokeStyle = d.o.type === "Kabel" ? cableColor : d.color ctx.strokeStyle = d.o.type === "Kabel" ? cableColor : d.color
ctx.globalAlpha = d.o.isVPN ? 0.1 : 0.8 ctx.globalAlpha = d.o.type === "VPN" ? 0.1 : 0.8
ctx.lineWidth = d.o.isVPN ? 1.5 : 2.5 ctx.lineWidth = d.o.type === "VPN" ? 1.5 : 2.5
ctx.stroke() ctx.stroke()
}) })
@ -380,7 +376,6 @@ define(["d3"], function (d3) {
// -- draw clients -- // -- draw clients --
ctx.save() ctx.save()
ctx.beginPath() ctx.beginPath()
if (scale > 0.9)
nodes.filter(visibleNodes).forEach(function (d) { nodes.filter(visibleNodes).forEach(function (d) {
var clients = d.o.node.statistics.clients var clients = d.o.node.statistics.clients
if (clients === 0) if (clients === 0)
@ -475,34 +470,32 @@ define(["d3"], function (d3) {
requestAnimationFrame(redraw) requestAnimationFrame(redraw)
} }
function distance(ax, ay, bx, by) { function distance(a, b) {
return Math.pow(ax - bx, 2) + Math.pow(ay - by, 2) return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)
} }
function distancePoint(a, b) { function distancePoint(a, b) {
return Math.sqrt(distance(a.x, a.y, b.x, b.y)) return Math.sqrt(distance(a, b))
} }
function distanceLink(p, a, b) { function distanceLink(p, a, b) {
/* http://stackoverflow.com/questions/849211 */ /* http://stackoverflow.com/questions/849211 */
var bx = b.x - ((b.x - a.x) / 2) var l2 = distance(a, b)
var by = b.y - ((b.y - a.y) / 2)
var l2 = distance(a.x, a.y, bx, by)
if (l2 === 0) if (l2 === 0)
return distance(p.x, p.y, a.x, a.y) return distance(p, a)
var t = ((p.x - a.x) * (bx - a.x) + (p.y - a.y) * (by - a.y)) / l2 var t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2
if (t < 0) if (t < 0)
return distance(p.x, p.y, a.x, a.y) return distance(p, a)
if (t > 1) if (t > 1)
return distance(p.x, p.y, bx, by) return distance(p, b)
return Math.sqrt(distance(p.x, p.y, a.x + t * (bx - a.x), a.y + t * (by - a.y) )) return Math.sqrt(distance(p, { x: a.x + t * (b.x - a.x),
y: a.y + t * (b.y - a.y) }))
} }
function translateXY(d) { function translateXY(d) {
@ -530,7 +523,7 @@ define(["d3"], function (d3) {
} }
var links = intLinks.filter(function (d) { var links = intLinks.filter(function (d) {
return !d.o.isVPN return d.o.type !== "VPN"
}).filter(function (d) { }).filter(function (d) {
return distanceLink(e, d.source, d.target) < LINE_RADIUS return distanceLink(e, d.source, d.target) < LINE_RADIUS
}) })
@ -591,13 +584,13 @@ define(["d3"], function (d3) {
.charge(-250) .charge(-250)
.gravity(0.1) .gravity(0.1)
.linkDistance(function (d) { .linkDistance(function (d) {
if (d.o.isVPN) if (d.o.type === "VPN")
return 0 return 0
else else
return LINK_DISTANCE return LINK_DISTANCE
}) })
.linkStrength(function (d) { .linkStrength(function (d) {
if (d.o.isVPN) if (d.o.type === "VPN")
return 0 return 0
else else
return Math.max(0.5, 1 / d.o.tq) return Math.max(0.5, 1 / d.o.tq)
@ -651,7 +644,7 @@ define(["d3"], function (d3) {
e.source = newNodesDict[d.source.id] e.source = newNodesDict[d.source.id]
e.target = newNodesDict[d.target.id] e.target = newNodesDict[d.target.id]
if (d.isVPN) if (d.type === "VPN")
e.color = "rgba(255, 255, 255, " + (0.6 / d.tq) + ")" e.color = "rgba(255, 255, 255, " + (0.6 / d.tq) + ")"
else else
e.color = linkScale(d.tq).hex() e.color = linkScale(d.tq).hex()
@ -703,15 +696,6 @@ define(["d3"], function (d3) {
linksDict[d.o.id] = d linksDict[d.o.id] = d
}) })
intLinks.forEach(function (d) {
if (linksDict[d.target.o.node_id + "-" + d.source.o.node_id])
return
var obj = { source: d.target, target: d.source, o: { isVPN: d.o.isVPN, type: "dead", id: d.target.o.node_id + "-" + d.source.o.node_id, tq: 1 }, color: "rgba(255, 255, 255, 0.6)" }
intLinks.push(obj)
linksDict[d.target.o.node_id + "-" + d.source.o.node_id] = obj
})
intNodes.forEach(function (d) { intNodes.forEach(function (d) {
d.neighbours = Object.keys(d.neighbours).map(function (k) { d.neighbours = Object.keys(d.neighbours).map(function (k) {
return d.neighbours[k] return d.neighbours[k]

View file

@ -48,9 +48,6 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
} }
} }
var loader = document.getElementsByClassName("loader")[0]
loader.classList.add("hide")
contentDiv = document.createElement("div") contentDiv = document.createElement("div")
contentDiv.classList.add("content") contentDiv.classList.add("content")
document.body.appendChild(contentDiv) document.body.appendChild(contentDiv)
@ -60,7 +57,7 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
contentDiv.appendChild(buttons) contentDiv.appendChild(buttons)
var buttonToggle = document.createElement("button") var buttonToggle = document.createElement("button")
buttonToggle.textContent = "\uF133" buttonToggle.textContent = ""
buttonToggle.onclick = function () { buttonToggle.onclick = function () {
if (content.constructor === Map) if (content.constructor === Map)
router.view("g") router.view("g")

255
lib/infobox/charts.js Normal file
View file

@ -0,0 +1,255 @@
define(["c3", "d3"], function (c3, d3) {
var charts = function (node, config) {
this.node = node
this.chartConfig = config
this.chart = null
this.zoomConfig = {
"levels": [
{"label": "8h", "from": 8 * 3600, "interval": 15 * 60}
/*{"label": "24h", "from": "26h", "interval": "1h"},
{"label": "1m", "from": "1mon", "interval": "1d"},
{"label": "1y", "from": 31536000, "interval": 2628000}*/
]
}
this.zoomLevel = 0
this.c3Config = {
"size": {
"height": 240
},
padding: {
bottom: 30
},
"legend": {
"item": {
"onclick": function (id) {
this.api.hide()
this.api.show(id)
}
}
},
"tooltip": {
"format": {
"value": this.c3FormatToolTip.bind(this)
}
},
"axis": {
"x": {
"type": "timeseries",
"tick": {
"format": this.c3FormatXAxis.bind(this),
"rotate": -45
}
},
"y": {
"min": 0,
"padding": {
"bottom": 0
}
}
}
}
this.cache = []
this.init()
}
charts.prototype = {
init: function () {
// Workaround for endless loop bug
if (this.c3Config.axis.x.tick && this.c3Config.axis.x.tick.format && typeof this.c3Config.axis.x.tick.format === "function") {
if (this.c3Config.axis.x.tick.format && !this.c3Config.axis.x.tick._format)
this.c3Config.axis.x.tick._format = this.c3Config.axis.x.tick.format
this.c3Config.axis.x.tick.format = function (val) {
return this.c3Config.axis.x.tick._format(val)
}.bind(this)
}
// Configure metrics
this.c3Config.data = {
"keys": {
"x": "time",
"value": this.chartConfig.metrics.map(function (metric) {
return metric.id
})
},
"colors": this.chartConfig.metrics.reduce(function (collector, metric) {
collector[metric.id] = metric.color
return collector
}, {}),
"names": this.chartConfig.metrics.reduce(function (collector, metric) {
collector[metric.id] = metric.label
return collector
}, {}),
"hide": this.chartConfig.metrics.map(function (metric) {
return metric.id
}).filter(function (id) {
return id !== this.chartConfig.defaultMetric
}.bind(this))
}
},
render: function () {
var div = document.createElement("div")
div.classList.add("chart")
var h4 = document.createElement("h4")
h4.textContent = this.chartConfig.name
div.appendChild(h4)
// Render chart
this.load(function (data) {
div.appendChild(this.renderChart(data))
// Render zoom controls
if (this.zoomConfig.levels.length > 0)
div.appendChild(this.renderZoomControls())
}.bind(this))
return div
},
renderChart: function (data) {
this.c3Config.data.json = data
this.chart = c3.generate(this.c3Config)
return this.chart.element
},
updateChart: function (data) {
this.c3Config.data.json = data
this.chart.load(this.c3Config.data)
},
renderZoomControls: function () {
// Draw zoom controls
var zoomDiv = document.createElement("div")
zoomDiv.classList.add("zoom-buttons")
var zoomButtons = []
this.zoomConfig.levels.forEach(function (v, level) {
var btn = document.createElement("button")
btn.classList.add("zoom-button")
btn.setAttribute("data-zoom-level", level)
if (level === this.zoomLevel)
btn.classList.add("active")
btn.onclick = function () {
if (level !== this.zoomLevel) {
zoomButtons.forEach(function (v, k) {
if (level !== k)
v.classList.remove("active")
else
v.classList.add("active")
})
this.setZoomLevel(level)
}
}.bind(this)
btn.textContent = v.label
zoomButtons[level] = btn
zoomDiv.appendChild(btn)
}.bind(this))
return zoomDiv
},
setZoomLevel: function (level) {
if (level !== this.zoomLevel) {
this.zoomLevel = level
this.load(this.updateChart.bind(this))
}
},
load: function (callback) {
if (this.cache[this.zoomLevel])
callback(this.cache[this.zoomLevel])
else {
var urls = []
var id = this.node.nodeinfo.node_id
var unixStamp = Math.floor(Date.now() / 1000)
var zoomConfig = this.zoomConfig.levels[this.zoomLevel]
this.chartConfig.metrics.forEach(function(metric) {
var parameters = [
"start=" + (unixStamp - zoomConfig.from),
"end=" + unixStamp,
"step=" + zoomConfig.interval,
"query=" + metric.query.replace("{{NODE_ID}}", id)
]
var url = this.chartConfig.url + "?" + parameters.join("&")
urls.push(url)
}.bind(this))
Promise.all(urls.map(getJSON)).then(function (data) {
this.cache[this.zoomLevel] = this.parse(data)
callback(this.cache[this.zoomLevel])
}.bind(this))
}
},
parse: function (results) {
var data = []
results[0].data.result[0].values.forEach(function (tp) {
var time = {"time": new Date(tp[0] * 1000)}
results.forEach(function(result) {
var metric = this.chartConfig.metrics[results.indexOf(result)]
var index = results[0].data.result[0].values.indexOf(tp)
time[metric.id] = this.formatValue(metric.id, result.data.result[0].values[index][1])
}.bind(this))
data.push(time)
}.bind(this))
return data
},
c3FormatToolTip: function (d, ratio, id) {
switch (id) {
case "uptime":
return d.toFixed(1) + " Tage"
default:
return d
}
},
c3FormatXAxis: function (d) {
var pad = function (number, pad) {
var N = Math.pow(10, pad)
return number < N ? ("" + (N + number)).slice(1) : "" + number
}
switch (this.zoomLevel) {
case 0: // 8h
case 1: // 24h
return pad(d.getHours(), 2) + ":" + pad(d.getMinutes(), 2)
case 2: // 1m
return pad(d.getDate(), 2) + "." + pad(d.getMonth() + 1, 2)
case 3: // 1y
return ["Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"][d.getMonth()]
default:
break
}
},
formatValue: function (id, value) {
switch (id) {
case "loadavg":
return (d3.format(".2r")(value))
case "clientcount":
return (Math.ceil(value))
case "uptime":
return (value / 86400)
default:
return value
}
}
}
return charts
})

View file

@ -1,10 +1,8 @@
define(function () { define(function () {
function showStatImg(o, d) { function showStatImg(o, source, target) {
var subst = {} var subst = {}
subst["{SOURCE}"] = d.source.node_id subst["{SOURCE}"] = source
subst["{SOURCE_NAME}"] = d.source.node.nodeinfo.hostname ? d.source.node.nodeinfo.hostname : "unknown" subst["{TARGET}"] = target
subst["{TARGET}"] = d.target.node_id
subst["{TARGET_NAME}"] = d.target.node.nodeinfo.hostname ? d.target.node.nodeinfo.hostname : "unknown"
return showStat(o, subst) return showStat(o, subst)
} }
@ -37,12 +35,15 @@ define(function () {
attributeEntry(attributes, "Hardware", (hw1 != null ? hw1 : "unbekannt") + " " + (hw2 != null ? hw2 : "unbekannt")) attributeEntry(attributes, "Hardware", (hw1 != null ? hw1 : "unbekannt") + " " + (hw2 != null ? hw2 : "unbekannt"))
el.appendChild(attributes) el.appendChild(attributes)
if (config.linkInfos) if (config.linkInfos) {
var source = d.source.node_id
var target = d.target.node_id
config.linkInfos.forEach( function (linkInfo) { config.linkInfos.forEach( function (linkInfo) {
var h4 = document.createElement("h4") var h4 = document.createElement("h4")
h4.textContent = linkInfo.name h4.textContent = linkInfo.name
el.appendChild(h4) el.appendChild(h4)
el.appendChild(showStatImg(linkInfo, d)) el.appendChild(showStatImg(linkInfo, source, target))
}) })
} }
}
}) })

View file

@ -1,83 +1,80 @@
define(function () { define(function () {
return function (config, el, router, d) { return function (config, el, router, d) {
var sidebarTitle = document.createElement("h2") var h2 = document.createElement("h2")
sidebarTitle.textContent = "Location: " + d.toString() h2.textContent = "Location: " + d.toString()
el.appendChild(sidebarTitle) el.appendChild(h2)
getJSON("https://nominatim.openstreetmap.org/reverse?format=json&lat=" + d.lat + "&lon=" + d.lng + "&zoom=18&addressdetails=0") getJSON("https://nominatim.openstreetmap.org/reverse?format=json&lat=" + d.lat + "&lon=" + d.lng + "&zoom=18&addressdetail=0")
.then(function(result) { .then(function(result) {
if(result.display_name) h2.textContent = result.display_name
sidebarTitle.textContent = result.display_name
}) })
var editLat = document.createElement("input") var latDiv = document.createElement("div")
editLat.type = "text" var h3lat = document.createElement("h3")
editLat.value = d.lat.toFixed(9) h3lat.textContent = "Breitengrad"
el.appendChild(createBox("lat", "Breitengrad", editLat)) h3lat.id = "h3-latitude"
latDiv.appendChild(h3lat)
var txt1 = document.createElement("textarea")
txt1.id = "location-latitude"
txt1.value = d.lat.toFixed(9)
var p = document.createElement("p")
p.appendChild(txt1)
p.appendChild(createCopyButton(txt1.id))
latDiv.appendChild(p)
el.appendChild(latDiv)
var editLng = document.createElement("input") var longDiv = document.createElement("div")
editLng.type = "text" var h3lng = document.createElement("h3")
editLng.value = d.lng.toFixed(9) h3lng.textContent = "Längengrad"
el.appendChild(createBox("lng", "Längengrad", editLng)) longDiv.appendChild(h3lng)
var txt2 = document.createElement("textarea")
txt2.id = "location-longitude"
txt2.value = d.lng.toFixed(9)
var p2 = document.createElement("p")
p2.appendChild(txt2)
p2.appendChild(createCopyButton(txt2.id))
longDiv.appendChild(p2)
longDiv.id = "div-longitude"
el.appendChild(longDiv)
var editUci = document.createElement("textarea") var a1 = document.createElement("a")
editUci.value = a1.textContent = "plain"
"uci set gluon-node-info.@location[0]='location'; " + a1.onclick = function() {
"uci set gluon-node-info.@location[0].share_location='1';" +
"uci set gluon-node-info.@location[0].latitude='" + d.lat.toFixed(9) + "';" +
"uci set gluon-node-info.@location[0].longitude='" + d.lng.toFixed(9) + "';" +
"uci commit gluon-node-info"
el.appendChild(createBox("uci", "Befehl", editUci, false))
var linkPlain = document.createElement("a")
linkPlain.textContent = "plain"
linkPlain.onclick = function() {
switch2plain() switch2plain()
return false return false
} }
linkPlain.href = "#" a1.href = "#"
var a2 = document.createElement("a")
var linkUci = document.createElement("a") a2.textContent = "uci"
linkUci.textContent = "uci" a2.onclick = function() {
linkUci.onclick = function() {
switch2uci() switch2uci()
return false return false
} }
linkUci.href = "#" a2.href = "#"
var hintText = document.createElement("p") var p3 = document.createElement("p")
hintText.appendChild(document.createTextNode("Du kannst zwischen ")) p3.textContent = "Du kannst zwischen "
hintText.appendChild(linkPlain) p3.appendChild(a1)
hintText.appendChild(document.createTextNode(" und ")) var t1 = document.createTextNode(" und ")
hintText.appendChild(linkUci) p3.appendChild(t1)
hintText.appendChild(document.createTextNode(" wechseln.")) p3.appendChild(a2)
el.appendChild(hintText) var t2 = document.createTextNode(" wechseln.")
p3.appendChild(t2)
el.appendChild(p3)
function createBox(name, title, inputElem, isVisible) { function createCopyButton(id) {
var visible = typeof isVisible !== "undefined" ? isVisible : true
var box = document.createElement("div")
var heading = document.createElement("h3")
heading.textContent = title
box.appendChild(heading)
var btn = document.createElement("button") var btn = document.createElement("button")
btn.className = "ion-ios-copy" btn.className = "ion-ios-copy"
btn.title = "Kopieren" btn.title = "Kopieren"
btn.onclick = function() { copy2clip(inputElem.id) } btn.onclick = function() {
inputElem.id = "location-" + name copy2clip(id)
inputElem.readOnly = true }
var line = document.createElement("p") return btn
line.appendChild(inputElem)
line.appendChild(btn)
box.appendChild(line)
box.id = "box-" + name
box.style.display = visible ? "block" : "none"
return box
} }
function copy2clip(id) { function copy2clip(id) {
var copyField = document.querySelector("#" + id) var copyTextarea = document.querySelector("#" + id)
copyField.select() copyTextarea.select()
try { try {
document.execCommand("copy") document.execCommand("copy")
} catch (err) { } catch (err) {
@ -86,15 +83,18 @@ define(function () {
} }
function switch2plain() { function switch2plain() {
document.getElementById("box-uci").style.display = "none" var box1 = document.getElementById("location-latitude")
document.getElementById("box-lat").style.display = "block" box1.value = d.lat.toFixed(9)
document.getElementById("box-lng").style.display = "block" var box2 = document.getElementById("location-longitude")
box2.value = d.lng.toFixed(9)
document.getElementById("h3-latitude").textContent = "Breitengrad"
document.getElementById("div-longitude").style.display = "block"
} }
function switch2uci() { function switch2uci() {
document.getElementById("box-uci").style.display = "block" document.getElementById("location-latitude").value = "uci set gluon-node-info.@location[0]='location'; uci set gluon-node-info.@location[0].share_location='1'; uci set gluon-node-info.@location[0].latitude='" + d.lat.toFixed(9) + "'; uci set gluon-node-info.@location[0].longitude='" + d.lng.toFixed(9) + "'; uci commit gluon-node-info"
document.getElementById("box-lat").style.display = "none" document.getElementById("h3-latitude").textContent = "Befehl"
document.getElementById("box-lng").style.display = "none" document.getElementById("div-longitude").style.display = "none"
} }
} }
}) })

View file

@ -1,5 +1,5 @@
define(["moment", "numeral", "tablesort", "tablesort.numeric"], define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"],
function (moment, numeral, Tablesort) { function (moment, numeral, Tablesort, Charts) {
function showGeoURI(d) { function showGeoURI(d) {
function showLatitude(d) { function showLatitude(d) {
var suffix = Math.sign(d) > -1 ? "'N" : "'S" var suffix = Math.sign(d) > -1 ? "'N" : "'S"
@ -81,124 +81,18 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
return d.firstseen.fromNow(true) return d.firstseen.fromNow(true)
} }
function wifiChannelAlias(ch) {
var chlist = {
"1": "2412 MHz",
"2": "2417 MHz",
"3": "2422 MHz",
"4": "2427 MHz",
"5": "2432 MHz",
"6": "2437 MHz",
"7": "2442 MHz",
"8": "2447 MHz",
"9": "2452 MHz",
"10": "2457 MHz",
"11": "2462 MHz",
"12": "2467 MHz",
"13": "2472 MHz",
"36": "5180 MHz (Indoors)",
"40": "5200 MHz (Indoors)",
"44": "5220 MHz (Indoors)",
"48": "5240 MHz (Indoors)",
"52": "5260 MHz (Indoors/DFS/TPC)",
"56": "5280 MHz (Indoors/DFS/TPC)",
"60": "5300 MHz (Indoors/DFS/TPC)",
"64": "5320 MHz (Indoors/DFS/TPC)",
"100": "5500 MHz (DFS) !!",
"104": "5520 MHz (DFS) !!",
"108": "5540 MHz (DFS) !!",
"112": "5560 MHz (DFS) !!",
"116": "5580 MHz (DFS) !!",
"120": "5600 MHz (DFS) !!",
"124": "5620 MHz (DFS) !!",
"128": "5640 MHz (DFS) !!",
"132": "5660 MHz (DFS) !!",
"136": "5680 MHz (DFS) !!",
"140": "5700 MHz (DFS) !!"
}
if (!(ch in chlist))
return ""
else
return chlist[ch]
}
function showWifiChannel(ch) {
if (!ch)
return undefined
return ch + " (" + wifiChannelAlias(ch) + ")"
}
function showClients(d) { function showClients(d) {
if (!d.flags.online) if (!d.flags.online)
return undefined return undefined
var meshclients = getMeshClients(d)
resetMeshClients(d)
var before = " ("
var after = " in der lokalen Wolke)"
return function (el) { return function (el) {
el.appendChild(document.createTextNode(d.statistics.clients > 0 ? d.statistics.clients : "keine")) el.appendChild(document.createTextNode(d.statistics.clients > 0 ? d.statistics.clients : "keine"))
el.appendChild(document.createTextNode(before))
el.appendChild(document.createTextNode(meshclients > 0 ? meshclients : "keine"))
el.appendChild(document.createTextNode(after))
el.appendChild(document.createElement("br")) el.appendChild(document.createElement("br"))
var span = document.createElement("span") var span = document.createElement("span")
span.classList.add("clients") span.classList.add("clients")
span.textContent = " ".repeat(d.statistics.clients) span.textContent = " ".repeat(d.statistics.clients)
el.appendChild(span) el.appendChild(span)
var spanmesh = document.createElement("span")
spanmesh.classList.add("clientsMesh")
spanmesh.textContent = " ".repeat(meshclients - d.statistics.clients)
el.appendChild(spanmesh)
}
}
function getMeshClients(node) {
var meshclients = 0
if (node.statistics && !isNaN(node.statistics.clients))
meshclients = node.statistics.clients
if (!node)
return 0
if (node.parsed)
return 0
node.parsed = 1
node.neighbours.forEach(function (neighbour) {
if (!neighbour.link.isVPN && neighbour.node)
meshclients += getMeshClients(neighbour.node)
})
return meshclients
}
function resetMeshClients(node) {
if (!node.parsed)
return
node.parsed = 0
node.neighbours.forEach(function (neighbour) {
if (!neighbour.link.isVPN && neighbour.node)
resetMeshClients(neighbour.node)
})
return
}
function showMeshClients(d) {
if (!d.flags.online)
return undefined
var meshclients = getMeshClients(d)
resetMeshClients(d)
return function (el) {
el.appendChild(document.createTextNode(meshclients > 0 ? meshclients : "keine"))
el.appendChild(document.createElement("br"))
} }
} }
@ -218,9 +112,6 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
if (link) { if (link) {
var a = document.createElement("a") var a = document.createElement("a")
if (ip.includes("."))
a.href = "http://" + ip + "/"
else
a.href = "http://[" + ip + "]/" a.href = "http://[" + ip + "]/"
a.textContent = ip a.textContent = ip
el.appendChild(a) el.appendChild(a)
@ -265,7 +156,7 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
} }
var label = document.createElement("label") var label = document.createElement("label")
label.textContent = +(Math.round(v + "e+2") + "e-2") label.textContent = (v)
span.appendChild(label) span.appendChild(label)
return span return span
@ -289,65 +180,6 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
} }
} }
function showAirtime(band, val) {
if (!val)
return undefined
return function (el) {
el.appendChild(showBar("airtime" + band.toString(), val))
}
}
function createLink(target, router) {
if (!target) return document.createTextNode("unknown")
var unknown = !(target.node)
var text = unknown ? (target.id ? target.id : target) : target.node.nodeinfo.hostname
if (!unknown) {
var link = document.createElement("a")
link.classList.add("hostname-link")
link.href = "#"
link.onclick = router.node(target.node)
link.textContent = text
return link
}
return document.createTextNode(text)
}
function showGateway(d, router) {
var nh
if (dictGet(d.statistics, ["nexthop"]))
nh = dictGet(d.statistics, ["nexthop"])
if (dictGet(d.statistics, ["gateway_nexthop"]))
nh = dictGet(d.statistics, ["gateway_nexthop"])
var gw = dictGet(d.statistics, ["gateway"])
if (!gw) return null
return function (el) {
var num = 0
while (gw && nh && gw.id !== nh.id && num < 10) {
if (num !== 0) el.appendChild(document.createTextNode(" -> "))
el.appendChild(createLink(nh, router))
num++
if (!nh.node || !nh.node.statistics) break
if (!dictGet(nh.node.statistics, ["gateway"]) || !dictGet(nh.node.statistics, ["gateway"]).id) break
if (dictGet(nh.node.statistics, ["gateway"]).id !== gw.id) break
if (dictGet(nh.node.statistics, ["gateway_nexthop"]))
nh = dictGet(nh.node.statistics, ["gateway_nexthop"])
else if (dictGet(nh.node.statistics, ["nexthop"]))
nh = dictGet(nh.node.statistics, ["nexthop"])
else
break
}
if (gw && nh && gw.id !== nh.id) {
if (num !== 0) el.appendChild(document.createTextNode(" -> "))
num++
el.appendChild(document.createTextNode("..."))
}
if (num !== 0) el.appendChild(document.createTextNode(" -> "))
el.appendChild(createLink(gw, router))
}
}
function showPages(d) { function showPages(d) {
var webpages = dictGet(d.nodeinfo, ["pages"]) var webpages = dictGet(d.nodeinfo, ["pages"])
if (webpages === null) if (webpages === null)
@ -387,37 +219,6 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
return au.enabled ? "aktiviert (" + au.branch + ")" : "deaktiviert" return au.enabled ? "aktiviert (" + au.branch + ")" : "deaktiviert"
} }
function showNodeImg(o, model) {
if (!model)
return document.createTextNode("Knotenname")
var content, caption
var modelhash = model.split("").reduce(function(a, b) {
a = ((a << 5) - a) + b.charCodeAt(0)
return a & a
}, 0)
content = document.createElement("img")
content.id = "routerpicture"
content.classList.add("nodeImg")
content.src = o.thumbnail.replace("{MODELHASH}", modelhash)
content.onerror = function() {
document.getElementById("routerpicdiv").outerHTML = "Knotenname"
}
if (o.caption) {
caption = o.caption.replace("{MODELHASH}", modelhash)
if (!content)
content = document.createTextNode(caption)
}
var p = document.createElement("p")
p.appendChild(content)
return content
}
function showStatImg(o, d) { function showStatImg(o, d) {
var subst = {} var subst = {}
subst["{NODE_ID}"] = d.nodeinfo.node_id ? d.nodeinfo.node_id : "unknown" subst["{NODE_ID}"] = d.nodeinfo.node_id ? d.nodeinfo.node_id : "unknown"
@ -426,29 +227,12 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
} }
return function(config, el, router, d) { return function(config, el, router, d) {
var attributes = document.createElement("table")
attributes.classList.add("attributes")
if (config.hwImg) {
var top = document.createElement("div")
top.id = "routerpicdiv"
try {
config.hwImg.forEach(function(hwImg) {
try {
top.appendChild(showNodeImg(hwImg, dictGet(d, ["nodeinfo", "hardware", "model"])))
} catch (err) {
console.log(err.message)
}
})
} catch (err) {
console.log(err.message)
}
attributeEntry(attributes, top, d.nodeinfo.hostname)
} else {
var h2 = document.createElement("h2") var h2 = document.createElement("h2")
h2.textContent = d.nodeinfo.hostname h2.textContent = d.nodeinfo.hostname
el.appendChild(h2) el.appendChild(h2)
}
var attributes = document.createElement("table")
attributes.classList.add("attributes")
attributeEntry(attributes, "Status", showStatus(d)) attributeEntry(attributes, "Status", showStatus(d))
attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null) attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null)
@ -464,20 +248,20 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
attributeEntry(attributes, "Site", showSite(d, config)) attributeEntry(attributes, "Site", showSite(d, config))
attributeEntry(attributes, "Uptime", showUptime(d)) attributeEntry(attributes, "Uptime", showUptime(d))
attributeEntry(attributes, "Teil des Netzes", showFirstseen(d)) attributeEntry(attributes, "Teil des Netzes", showFirstseen(d))
attributeEntry(attributes, "Kanal 2.4 GHz", showWifiChannel(dictGet(d.nodeinfo, ["wireless", "chan2"])))
attributeEntry(attributes, "Kanal 5 GHz", showWifiChannel(dictGet(d.nodeinfo, ["wireless", "chan5"])))
attributeEntry(attributes, "Airtime 2.4 GHz", showAirtime(2, dictGet(d.statistics, ["wireless", "airtime2"])))
attributeEntry(attributes, "Airtime 5 GHz", showAirtime(5, dictGet(d.statistics, ["wireless", "airtime5"])))
attributeEntry(attributes, "Systemlast", showLoad(d)) attributeEntry(attributes, "Systemlast", showLoad(d))
attributeEntry(attributes, "Arbeitsspeicher", showRAM(d)) attributeEntry(attributes, "Arbeitsspeicher", showRAM(d))
attributeEntry(attributes, "IP Adressen", showIPs(d)) attributeEntry(attributes, "IP Adressen", showIPs(d))
attributeEntry(attributes, "Webseite", showPages(d)) attributeEntry(attributes, "Webseite", showPages(d))
attributeEntry(attributes, "Gewähltes Gateway", showGateway(d, router)) attributeEntry(attributes, "Gewähltes Gateway", dictGet(d.statistics, ["gateway"]))
attributeEntry(attributes, "Autom. Updates", showAutoupdate(d)) attributeEntry(attributes, "Autom. Updates", showAutoupdate(d))
attributeEntry(attributes, "Clients", showClients(d), showMeshClients(d)) attributeEntry(attributes, "Clients", showClients(d))
el.appendChild(attributes) el.appendChild(attributes)
if (!d.flags.gateway && config.nodeCharts)
config.nodeCharts.forEach( function (config) {
el.appendChild((new Charts(d, config)).render())
})
if (config.nodeInfos) if (config.nodeInfos)
config.nodeInfos.forEach( function (nodeInfo) { config.nodeInfos.forEach( function (nodeInfo) {
@ -531,7 +315,13 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
tr.appendChild(td1) tr.appendChild(td1)
var td2 = document.createElement("td") var td2 = document.createElement("td")
td2.appendChild(createLink(d, router)) var a1 = document.createElement("a")
a1.classList.add("hostname")
a1.textContent = unknown ? d.id : d.node.nodeinfo.hostname
if (!unknown)
a1.href = "#"
a1.onclick = router.node(d.node)
td2.appendChild(a1)
if (!unknown && has_location(d.node)) { if (!unknown && has_location(d.node)) {
var span = document.createElement("span") var span = document.createElement("span")

View file

@ -4,14 +4,12 @@ function (moment, Router, L, GUI, numeral) {
function handleData(data) { function handleData(data) {
var dataNodes = {} var dataNodes = {}
dataNodes.nodes = [] dataNodes.nodes = []
dataNodes.nodeIds = []
var dataGraph = {} var dataGraph = {}
dataGraph.batadv = {} dataGraph.batadv = {}
dataGraph.batadv.nodes = [] dataGraph.batadv.nodes = []
dataGraph.batadv.links = [] dataGraph.batadv.links = []
function rearrangeLinks(d) { function rearrangeLinks(d) {
d.source += dataGraph.batadv.nodes.length d.source += dataGraph.batadv.nodes.length
d.target += dataGraph.batadv.nodes.length d.target += dataGraph.batadv.nodes.length
@ -34,22 +32,11 @@ function (moment, Router, L, GUI, numeral) {
vererr = "Unsupported nodes version: " + data[i].version vererr = "Unsupported nodes version: " + data[i].version
console.log(vererr) //silent fail console.log(vererr) //silent fail
} else { } else {
data[i].nodes.forEach(fillData) dataNodes.nodes = dataNodes.nodes.concat(data[i].nodes)
dataNodes.timestamp = data[i].timestamp dataNodes.timestamp = data[i].timestamp
} }
} }
function fillData (node) {
var position = dataNodes.nodeIds.indexOf(node.nodeinfo.node_id)
if(position === -1){
dataNodes.nodes.push(node)
dataNodes.nodeIds.push(node.nodeinfo.node_id)
}
else
if(node.flags.online === true)
dataNodes.nodes[position] = node
}
var nodes = dataNodes.nodes.filter( function (d) { var nodes = dataNodes.nodes.filter( function (d) {
return "firstseen" in d && "lastseen" in d return "firstseen" in d && "lastseen" in d
}) })
@ -123,44 +110,17 @@ function (moment, Router, L, GUI, numeral) {
nodes.forEach( function (d) { nodes.forEach( function (d) {
d.neighbours = [] d.neighbours = []
if (d.statistics) {
/*eslint camelcase:0*/
if ("gateway" in d.statistics && d.statistics.gateway in graphnodes)
d.statistics.gateway = {"node": graphnodes[d.statistics.gateway], "id": d.statistics.gateway}
if ("nexthop" in d.statistics && d.statistics.nexthop in graphnodes)
d.statistics.nexthop = {"node": graphnodes[d.statistics.nexthop], "id": d.statistics.nexthop}
if ("gateway_nexthop" in d.statistics && d.statistics.gateway_nexthop in graphnodes)
d.statistics.gateway_nexthop = {"node": graphnodes[d.statistics.gateway_nexthop], "id": d.statistics.gateway_nexthop}
}
}) })
links.forEach( function (d) { links.forEach( function (d) {
if (d.type === "tunnel" || d.vpn) { if (d.type === "tunnel")
d.type = "VPN" d.type = "VPN"
d.isVPN = true else if (d.type === "wireless")
} else if (d.type === "fastd") {
d.type = "fastd"
d.isVPN = true
} else if (d.type === "l2tp") {
d.type = "L2TP"
d.isVPN = true
} else if (d.type === "gre") {
d.type = "GRE"
d.isVPN = true
} else if (d.type === "wireless") {
d.type = "Wifi" d.type = "Wifi"
d.isVPN = false else if (d.type === "other")
} else if (d.type === "other") {
d.type = "Kabel" d.type = "Kabel"
d.isVPN = false else
} else {
d.type = "N/A" d.type = "N/A"
d.isVPN = false
}
if (d.isVPN && d.target.node)
d.target.node.flags.uplink = true
var unknown = (d.source.node === undefined) var unknown = (d.source.node === undefined)
if (unknown) { if (unknown) {
d.target.node.neighbours.push({ id: d.source.id, link: d, incoming: true }) d.target.node.neighbours.push({ id: d.source.id, link: d, incoming: true })
@ -168,7 +128,7 @@ function (moment, Router, L, GUI, numeral) {
} }
d.source.node.neighbours.push({ node: d.target.node, link: d, incoming: false }) d.source.node.neighbours.push({ node: d.target.node, link: d, incoming: false })
d.target.node.neighbours.push({ node: d.source.node, link: d, incoming: true }) d.target.node.neighbours.push({ node: d.source.node, link: d, incoming: true })
if (!d.isVPN) if (d.type !== "VPN")
d.source.node.meshlinks = d.source.node.meshlinks ? d.source.node.meshlinks + 1 : 1 d.source.node.meshlinks = d.source.node.meshlinks ? d.source.node.meshlinks + 1 : 1
}) })

View file

@ -18,7 +18,7 @@ define(["map/clientlayer", "map/labelslayer",
onAdd: function () { onAdd: function () {
var button = L.DomUtil.create("button", "add-layer") var button = L.DomUtil.create("button", "add-layer")
button.textContent = "\uF2C7" button.textContent = ""
// L.DomEvent.disableClickPropagation(button) // L.DomEvent.disableClickPropagation(button)
// Click propagation isn't disabled as this causes problems with the // Click propagation isn't disabled as this causes problems with the
@ -46,7 +46,7 @@ define(["map/clientlayer", "map/labelslayer",
onAdd: function () { onAdd: function () {
var button = L.DomUtil.create("button", "locate-user") var button = L.DomUtil.create("button", "locate-user")
button.textContent = "\uF2E9" button.textContent = ""
L.DomEvent.disableClickPropagation(button) L.DomEvent.disableClickPropagation(button)
L.DomEvent.addListener(button, "click", this.onClick, this) L.DomEvent.addListener(button, "click", this.onClick, this)
@ -85,7 +85,7 @@ define(["map/clientlayer", "map/labelslayer",
onAdd: function () { onAdd: function () {
var button = L.DomUtil.create("button", "coord-picker") var button = L.DomUtil.create("button", "coord-picker")
button.textContent = "\uF2A6" button.textContent = ""
// Click propagation isn't disabled as this causes problems with the // Click propagation isn't disabled as this causes problems with the
// location picking mode; instead propagation is stopped in onClick(). // location picking mode; instead propagation is stopped in onClick().
@ -172,7 +172,7 @@ define(["map/clientlayer", "map/labelslayer",
var map, userLocation var map, userLocation
var layerControl var layerControl
var customLayers = {} var customLayers = new Set()
var baseLayers = {} var baseLayers = {}
var locateUserButton = new LocateButton(function (d) { var locateUserButton = new LocateButton(function (d) {
@ -259,16 +259,16 @@ define(["map/clientlayer", "map/labelslayer",
if (layerName in baseLayers) if (layerName in baseLayers)
return return
if (layerName in customLayers) if (customLayers.has(layerName))
return return
try { try {
var layer = L.tileLayer.provider(layerName) var layer = L.tileLayer.provider(layerName)
layerControl.addBaseLayer(layer, layerName) layerControl.addBaseLayer(layer, layerName)
customLayers[layerName] = layer customLayers.add(layerName)
if (localStorageTest()) if (localStorageTest())
localStorage.setItem("map/customLayers", JSON.stringify(Object.keys(customLayers))) localStorage.setItem("map/customLayers", JSON.stringify(Array.from(customLayers)))
} catch (e) { } catch (e) {
return return
} }
@ -286,10 +286,16 @@ define(["map/clientlayer", "map/labelslayer",
var layers = config.mapLayers.map( function (d) { var layers = config.mapLayers.map( function (d) {
return { return {
"name": d.name, "name": d.name,
"layer": "url" in d ? "layers" in d.config ? L.tileLayer.wms(d.url, d.config) : L.tileLayer(d.url, d.config) : L.tileLayer.provider(d.name) "layer": "url" in d ? L.tileLayer(d.url, d.config) : L.tileLayer.provider(d.name)
} }
}) })
var maxLayerZoom = Math.max.apply(Math, config.mapLayers.map(
function(d) {
return (typeof d.config !== "undefined" && typeof d.config.maxZoom !== "undefined") ? d.config.maxZoom : 18
}))
layers[0].layer.addTo(map) layers[0].layer.addTo(map)
layers.forEach( function (d) { layers.forEach( function (d) {
@ -318,33 +324,16 @@ define(["map/clientlayer", "map/labelslayer",
if (d) if (d)
d.forEach(addLayer) d.forEach(addLayer)
d = JSON.parse(localStorage.getItem("map/selectedLayer"))
d = d && d.name in baseLayers ? baseLayers[d.name] : d && d.name in customLayers ? customLayers[d.name] : false
if (d) {
map.removeLayer(layers[0].layer)
map.addLayer(d)
}
} }
var clientLayer = new ClientLayer({minZoom: 15}) var clientLayer = new ClientLayer({minZoom: 15, maxZoom: maxLayerZoom})
clientLayer.addTo(map) clientLayer.addTo(map)
clientLayer.setZIndex(5) clientLayer.setZIndex(5)
var labelsLayer = new LabelsLayer({}) var labelsLayer = new LabelsLayer({maxZoom: maxLayerZoom})
labelsLayer.addTo(map) labelsLayer.addTo(map)
labelsLayer.setZIndex(6) labelsLayer.setZIndex(6)
map.on("baselayerchange", function(e) {
map.options.maxZoom = e.layer.options.maxZoom
clientLayer.options.maxZoom = map.options.maxZoom
labelsLayer.options.maxZoom = map.options.maxZoom
if (map.getZoom() > map.options.maxZoom) map.setZoom(map.options.maxZoom)
if (localStorageTest())
localStorage.setItem("map/selectedLayer", JSON.stringify({name: e.name}))
})
var nodeDict = {} var nodeDict = {}
var linkDict = {} var linkDict = {}
var highlight var highlight

View file

@ -1,12 +1,15 @@
define(["leaflet"], define(["leaflet", "jshashes"],
function (L) { function (L, jsHashes) {
var MD5 = new jsHashes.MD5()
return L.TileLayer.Canvas.extend({ return L.TileLayer.Canvas.extend({
setData: function (d) { setData: function (d) {
this.data = d this.data = d
//pre-calculate start angles //pre-calculate start angles
this.data.all().forEach(function (d) { this.data.all().forEach(function (d) {
d.startAngle = (parseInt(d.node.nodeinfo.node_id.substr(10, 2), 16) / 255) * 2 * Math.PI var hash = MD5.hex(d.node.nodeinfo.node_id)
d.startAngle = (parseInt(hash.substr(0, 2), 16) / 255) * 2 * Math.PI
}) })
this.redraw() this.redraw()
}, },

View file

@ -6,22 +6,16 @@ define(function () {
self.setData = function (d) { self.setData = function (d) {
var totalNodes = sum(d.nodes.all.map(one)) var totalNodes = sum(d.nodes.all.map(one))
var totalOnlineNodes = sum(d.nodes.all.filter(online).map(one)) var totalOnlineNodes = sum(d.nodes.all.filter(online).map(one))
var totalOfflineNodes = sum(d.nodes.all.filter(function (node) {return !node.flags.online}).map(one))
var totalNewNodes = sum(d.nodes.new.map(one)) var totalNewNodes = sum(d.nodes.new.map(one))
var totalLostNodes = sum(d.nodes.lost.map(one)) var totalLostNodes = sum(d.nodes.lost.map(one))
var totalClients = sum(d.nodes.all.filter(online).map( function (d) { var totalClients = sum(d.nodes.all.filter(online).map( function (d) {
return d.statistics.clients ? d.statistics.clients : 0 return d.statistics.clients
})) }))
var totalGateways = sum(Array.from(new Set(d.nodes.all.filter(online).map( function(d) { var totalGateways = sum(d.nodes.all.filter(online).filter( function (d) {
return ("gateway" in d.statistics && d.statistics.gateway.id) ? d.statistics.gateway.id : d.statistics.gateway
}).concat(d.nodes.all.filter( function (d) {
return d.flags.gateway return d.flags.gateway
})))).map(function(d) { }).map(one))
return (typeof d === "string") ? 1 : 0
}))
var nodetext = [{ count: totalOnlineNodes, label: "online" }, var nodetext = [{ count: totalOnlineNodes, label: "online" },
{ count: totalOfflineNodes, label: "offline" },
{ count: totalNewNodes, label: "neu" }, { count: totalNewNodes, label: "neu" },
{ count: totalLostNodes, label: "verschwunden" } { count: totalLostNodes, label: "verschwunden" }
].filter( function (d) { return d.count > 0 } ) ].filter( function (d) { return d.count > 0 } )

View file

@ -21,11 +21,7 @@ define(["sorttable", "virtual-dom", "numeral"], function (SortTable, V, numeral)
var headings = [{ name: "Knoten", var headings = [{ name: "Knoten",
sort: function (a, b) { sort: function (a, b) {
var aname = typeof a.nodeinfo.hostname === "string" ? a.nodeinfo.hostname : a.nodeinfo.node_id return a.nodeinfo.hostname.localeCompare(b.nodeinfo.hostname)
var bname = typeof b.nodeinfo.hostname === "string" ? b.nodeinfo.hostname : b.nodeinfo.node_id
if (typeof aname === "string" && typeof bname === "string")
return aname.localeCompare(bname)
return typeof aname === "string" ? 1 : typeof bname === "string" ? -1 : 0
}, },
reverse: false reverse: false
}, },
@ -56,7 +52,7 @@ define(["sorttable", "virtual-dom", "numeral"], function (SortTable, V, numeral)
td1Content.push(V.h("a", { className: aClass.join(" "), td1Content.push(V.h("a", { className: aClass.join(" "),
onclick: router.node(d), onclick: router.node(d),
href: "#!n:" + d.nodeinfo.node_id href: "#"
}, d.nodeinfo.hostname)) }, d.nodeinfo.hostname))
if (has_location(d)) if (has_location(d))

View file

@ -128,13 +128,7 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
return d ? "online" : "offline" return d ? "online" : "offline"
}) })
var fwDict = count(nodes, ["nodeinfo", "software", "firmware", "release"]) var fwDict = count(nodes, ["nodeinfo", "software", "firmware", "release"])
var hwDict = count(nodes, ["nodeinfo", "hardware", "model"], function (d) { var hwDict = count(nodes, ["nodeinfo", "hardware", "model"])
if (d) {
d = d.replace(/\(r\)|\(tm\)/gi, "").replace(/AMD |Intel |TP-Link | CPU| Processor/g, "")
if (d.indexOf("@") > 0) d = d.substring(0, d.indexOf("@"))
}
return d
})
var geoDict = count(nodes, ["nodeinfo", "location"], function (d) { var geoDict = count(nodes, ["nodeinfo", "location"], function (d) {
return d && d.longitude && d.latitude ? "ja" : "nein" return d && d.longitude && d.latitude ? "ja" : "nein"
}) })
@ -152,15 +146,12 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
return d ? "ja" : "nein" return d ? "ja" : "nein"
}) })
var gwNodesDict = count(onlineNodes, ["statistics", "gateway"], function (d) { var gwNodesDict = count(nodes, ["statistics", "gateway"], function (d) {
if (d === null) if (d === null)
return null return null
if (d.node) if (d in nodeDict)
return d.node.nodeinfo.hostname return nodeDict[d].nodeinfo.hostname
if (d.id)
return d.id
return d return d
}) })
@ -169,16 +160,13 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
if (d === null) if (d === null)
return null return null
if (d.node) if (d in nodeDict)
return d.node.nodeinfo.hostname return nodeDict[d].nodeinfo.hostname
if (d.id)
return d.id
return d return d
}) })
var siteDict = count(nodes, ["nodeinfo", "system", "site_code"], function (d) { var siteDict = count(onlineNodes, ["nodeinfo", "system", "site_code"], function (d) {
var rt = d var rt = d
if (config.siteNames) if (config.siteNames)
config.siteNames.forEach( function (t) { config.siteNames.forEach( function (t) {
@ -194,8 +182,8 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
fillTable("Koordinaten", geoTable, geoDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Koordinaten", geoTable, geoDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Uplink", uplinkTable, uplinkDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Uplink", uplinkTable, uplinkDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Autom. Updates", autoTable, autoDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Autom. Updates", autoTable, autoDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Gateway", gwNodesTable, gwNodesDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Nodes an Gateway", gwNodesTable, gwNodesDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Gateway", gwClientsTable, gwClientsDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Clients an Gateway", gwClientsTable, gwClientsDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Site", siteTable, siteDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Site", siteTable, siteDict.sort(function (a, b) { return b[1] - a[1] }))
} }

View file

@ -78,8 +78,6 @@ define(function () {
if (!s) if (!s)
return false return false
s = decodeURIComponent(s)
if (!s.startsWith("#!")) if (!s.startsWith("#!"))
return false return false
@ -96,7 +94,7 @@ define(function () {
var id var id
if (args[0] === "n") { if (args[0] === "n") {
id = args[1] id = decodeURIComponent(args[1])
if (id in objects.nodes) { if (id in objects.nodes) {
currentObject = { node: objects.nodes[id] } currentObject = { node: objects.nodes[id] }
gotoNode(objects.nodes[id]) gotoNode(objects.nodes[id])
@ -105,7 +103,7 @@ define(function () {
} }
if (args[0] === "l") { if (args[0] === "l") {
id = args[1] id = decodeURIComponent(args[1])
if (id in objects.links) { if (id in objects.links) {
currentObject = { link: objects.links[id] } currentObject = { link: objects.links[id] }
gotoLink(objects.links[id]) gotoLink(objects.links[id])

View file

@ -41,7 +41,7 @@ define(["moment", "virtual-dom"], function (moment, V) {
td1Content.push(V.h("a", { className: aClass.join(" "), td1Content.push(V.h("a", { className: aClass.join(" "),
onclick: router.node(d), onclick: router.node(d),
href: "#!n:" + d.nodeinfo.node_id href: "#"
}, d.nodeinfo.hostname)) }, d.nodeinfo.hostname))
if (has_location(d)) if (has_location(d))

View file

@ -1,41 +1,22 @@
{ {
"name": "hopglass", "name": "hopglass",
"version": "1.0.0",
"scripts": { "scripts": {
"test": "node -e \"require('grunt').cli()\" '' clean lint" "test": "node -e \"require('grunt').cli()\" '' clean lint"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^6.3.3", "grunt": "^0.4.5",
"dart-sass": "^1.16.1",
"grunt": "^1.0.3",
"grunt-check-dependencies": "^0.6.0", "grunt-check-dependencies": "^0.6.0",
"grunt-contrib-clean": "^0.6.0", "grunt-contrib-clean": "^0.6.0",
"grunt-contrib-connect": "^0.8.0", "grunt-contrib-connect": "^0.8.0",
"grunt-contrib-copy": "^0.5.0", "grunt-contrib-copy": "^0.5.0",
"grunt-contrib-cssmin": "^0.12.2", "grunt-contrib-cssmin": "^0.12.2",
"grunt-contrib-requirejs": "^0.4.4", "grunt-contrib-requirejs": "^0.4.4",
"grunt-contrib-sass": "^0.9.2",
"grunt-contrib-uglify": "^0.5.1", "grunt-contrib-uglify": "^0.5.1",
"grunt-contrib-watch": "^0.6.1", "grunt-contrib-watch": "^0.6.1",
"grunt-eslint": "^10.0.0", "grunt-eslint": "^10.0.0",
"grunt-git-describe": "^2.3.2", "grunt-bower-install-simple": "^1.1.2",
"grunt-postcss": "^0.7.2", "grunt-git-describe": "^2.3.2"
"grunt-sass": "^3.0.2"
},
"dependencies": {
"almond": "^0.3.3",
"chroma-js": "^0.7.8",
"d3": "^3.5.17",
"ionicons": "^2.0.1",
"leaflet": "^0.7.7",
"leaflet-label": "^0.2.1-0",
"leaflet-providers": "^1.5.0",
"moment": "^2.23.0",
"numeraljs": "^1.5.6",
"rbush": "^1.4.3",
"requirejs": "^2.3.2",
"roboto-fontface": "^0.10.0",
"tablesort": "3.0.2",
"virtual-dom": "^2.1.1"
}, },
"eslintConfig": { "eslintConfig": {
"env": { "env": {

View file

@ -28,7 +28,3 @@ h5 {
h6 { h6 {
font-size: 0.67em; font-size: 0.67em;
} }
.hide {
display: none;
}

28
scss/_chart.scss Normal file
View file

@ -0,0 +1,28 @@
.infobox .chart {
position: relative;
& > .c3 {
margin-right: 20px;
}
.zoom-buttons {
position: absolute;
top: 0px;
right: 20px;
button {
font-size: 10pt;
width: 3em;
height: 3em;
border-radius: 1.5em;
margin-left: 6px;
}
}
.c3-tooltip-container {
width: 150px;
.c3-tooltip {
border-collapse: collapse;
}
}
}

View file

@ -2,7 +2,7 @@
margin: 0; margin: 0;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
font-family: Roboto, sans-serif; font-family: Roboto;
font-size: 0.83em; font-size: 0.83em;
font-weight: bold; font-weight: bold;
padding: 0 6pt 6pt !important; padding: 0 6pt 6pt !important;
@ -10,7 +10,7 @@
li { li {
border-radius: 20pt; border-radius: 20pt;
display: flex; display: flex;
padding: 0 0 0 8pt; padding: 0pt 0 0pt 8pt;
margin: 3pt; margin: 3pt;
align-items: center; align-items: center;
background: #009ee0; background: #009ee0;

View file

@ -1 +1 @@
../node_modules/leaflet-label/dist/leaflet.label.css ../bower_components/Leaflet.label/dist/leaflet.label.css

View file

@ -1 +1 @@
../node_modules/leaflet/dist/leaflet.css ../bower_components/leaflet/dist/leaflet.css

View file

@ -1,4 +1,5 @@
.legend .symbol { .legend .symbol
{
width: 1em; width: 1em;
height: 1em; height: 1em;
border-radius: 50%; border-radius: 50%;
@ -6,18 +7,22 @@
vertical-align: -5%; vertical-align: -5%;
} }
.legend-new .symbol { .legend-new .symbol
{
background-color: #93E929; background-color: #93E929;
} }
.legend-online .symbol { .legend-online .symbol
{
background-color: #1566A9; background-color: #1566A9;
} }
.legend-offline .symbol { .legend-offline .symbol
{
background-color: #D43E2A; background-color: #D43E2A;
} }
.legend-online, .legend-offline { .legend-online, .legend-offline
{
margin-left: 1em; margin-left: 1em;
} }

View file

@ -1,23 +0,0 @@
.loader {
color: #000000;
font-size: 1.8em;
line-height: 2;
margin: 30vh auto;
text-align: center;
}
.spinner {
animation: .6s spinner ease-in-out infinite alternate;
border-bottom: 2px solid #000000;
border-radius: 50%;
display: inline-block;
height: 64px;
margin-top: 10px;
width: 64px;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}

View file

@ -35,25 +35,13 @@
} }
@-webkit-keyframes blink { @-webkit-keyframes blink {
0% { 0% { opacity: 1.0; }
opacity: 1.0; 80% { opacity: 1.0; }
} 90% { opacity: 0.0; }
80% {
opacity: 1.0;
}
90% {
opacity: 0.0;
}
} }
@keyframes blink { @keyframes blink {
0% { 0% { opacity: 1.0; }
opacity: 1.0; 80% { opacity: 1.0; }
} 90% { opacity: 0.0; }
80% {
opacity: 1.0;
}
90% {
opacity: 0.0;
}
} }

View file

@ -22,31 +22,25 @@ time, mark, audio, video {
font: inherit; font: inherit;
vertical-align: baseline; vertical-align: baseline;
} }
/* HTML5 display-role reset for older browsers */ /* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section { footer, header, hgroup, menu, nav, section {
display: block; display: block;
} }
body { body {
line-height: 1; line-height: 1;
} }
ol, ul { ol, ul {
list-style: none; list-style: none;
} }
blockquote, q { blockquote, q {
quotes: none; quotes: none;
} }
blockquote:before, blockquote:after, blockquote:before, blockquote:after,
q:before, q:after { q:before, q:after {
content: ''; content: '';
content: none; content: none;
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;

View file

@ -3,13 +3,17 @@
@mixin shadow($level:1){ @mixin shadow($level:1){
@if $level == 1 { @if $level == 1 {
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
} @else if $level == 2 { }
@else if $level == 2 {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
} @else if $level == 3 { }
@else if $level == 3 {
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
} @else if $level == 4 { }
@else if $level == 4 {
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
} @else if $level == 5 { }
@else if $level == 5 {
box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22); box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
} }
} }

View file

@ -1,36 +0,0 @@
.nodeheader {
width: 90%;
position: relative;
z-index: 2;
}
.nodeheader img {
width: 30%;
max-height: 180px;
margin: auto;
text-align: center;
max-width: 128px;
display: block;
z-index: 2;
}
.nodeheader img .none {
display: none;
}
.nodeheader span {
background-color: silver;
background-color: hsla(0, 0%, 100%, 0.5);
position: absolute;
bottom: 0;
width: 100%;
line-height: 1.5em;
text-align: center;
color: black;
font-weight: bold;
font-size: large;
z-index: 2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View file

@ -4,7 +4,6 @@
@import '_leaflet'; @import '_leaflet';
@import '_leaflet.label'; @import '_leaflet.label';
@import '_filters'; @import '_filters';
@import '_loader';
$minscreenwidth: 630pt; $minscreenwidth: 630pt;
$sidebarwidth: 420pt; $sidebarwidth: 420pt;
@ -15,6 +14,7 @@ $buttondistance: 12pt;
@import '_map'; @import '_map';
@import '_forcegraph'; @import '_forcegraph';
@import '_legend'; @import '_legend';
@import '_chart';
.content { .content {
position: fixed; position: fixed;
@ -45,7 +45,7 @@ $buttondistance: 12pt;
margin: 0; margin: 0;
list-style: none; list-style: none;
display: flex; display: flex;
font-family: Roboto, sans-serif; font-family: Roboto;
@include shadow(1); @include shadow(1);
} }
@ -70,9 +70,8 @@ $buttondistance: 12pt;
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: 'Roboto-Slab', serif; font-family: 'Roboto Slab', serif;
font-size: 11pt; font-size: 11pt;
color: #333;
} }
th.sort-header::selection { th.sort-header::selection {
@ -118,38 +117,6 @@ table.attributes td {
line-height: 1.41em; line-height: 1.41em;
} }
table.attributes tr.routerpic {
max-height: 128px;
max-width: 128px;
min-width: 128px;
min-height: 128px;
/*height: 128px;*/
/*background-color: green;*/
}
table.attributes tr.routerpic td {
font-weight: bold;
/*background-color: red;*/
font-size: large;
vertical-align:bottom;
}
table.attributes tr.routerpic th {
font-weight: bold;
/*background-color: red;*/
font-size: large;
vertical-align:bottom;
}
.nodenamesidebar {
position: relative;
font-weight: bold;
/*background-color: red;*/
font-size: large;
vertical-align:bottom;
bottom: 0px;
}
.sidebar { .sidebar {
.infobox, .container { .infobox, .container {
@include shadow(2); @include shadow(2);
@ -177,13 +144,6 @@ table.attributes tr.routerpic th {
white-space: normal; white-space: normal;
} }
.infobox .clientsMesh {
font-family: "ionicons";
color: #dbdbdb;
word-spacing: -0.2em;
white-space: normal;
}
.infobox { .infobox {
position: relative; position: relative;
padding: 0.25em 0; padding: 0.25em 0;
@ -192,27 +152,6 @@ table.attributes tr.routerpic th {
img { img {
max-width: 100%; max-width: 100%;
} }
input[type="text"], textarea {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
border-radius: 5px;
border: none;
line-height: 1.67em;
vertical-align: bottom;
margin-right: 0.7em;
padding: 3px 6px;
font-family: monospace;
width: 70%;
max-width: 500px;
min-height: 42px;
font-size: 1.15em;
}
textarea {
resize: vertical;
overflow: auto;
max-height: 300px;
}
} }
button { button {
@ -221,7 +160,6 @@ button {
@include shadow(1); @include shadow(1);
border-radius: 0.9em; border-radius: 0.9em;
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.7);
color: #333;
border: none; border: none;
cursor: pointer; cursor: pointer;
height: 1.8em; height: 1.8em;
@ -242,7 +180,7 @@ button:hover {
} }
button:active { button:active {
box-shadow: inset 0 5px 20px rgba(0, 0, 0, 0.19), inset 0 3px 6px rgba(0, 0, 0, 0.23); box-shadow: inset 0px 5px 20px rgba(0, 0, 0, 0.19), inset 0px 3px 6px rgba(0, 0, 0, 0.23);
} }
button::-moz-focus-inner { button::-moz-focus-inner {
@ -261,8 +199,6 @@ button.close {
border-radius: 0; border-radius: 0;
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
font-family: "ionicons"; font-family: "ionicons";
position: absolute;
right: 0;
&:hover { &:hover {
color: #dc0067; color: #dc0067;
@ -364,7 +300,7 @@ table {
position: absolute; position: absolute;
top: $buttondistance; top: $buttondistance;
left: $buttondistance; left: $buttondistance;
padding-bottom: $buttondistance; margin-bottom: $buttondistance;
transition: left 0.5s; transition: left 0.5s;
} }
@ -449,9 +385,9 @@ table {
@media screen and (max-width: 80em) { @media screen and (max-width: 80em) {
.sidebar { .sidebar {
font-size: 0.8em; font-size: 0.8em;
top: 0; top: 0pt;
left: 0; left: 0pt;
margin: 0; margin: 0pt;
width: $sidebarwidthsmall; width: $sidebarwidthsmall;
min-height: 100vh; min-height: 100vh;
@include shadow(2); @include shadow(2);
@ -482,7 +418,7 @@ table {
.sidebar { .sidebar {
position: static; position: static;
margin: 0 !important; margin: 0em !important;
width: auto; width: auto;
height: auto; height: auto;
min-height: 0; min-height: 0;

View file

@ -1,6 +1,6 @@
module.exports = function(grunt) { module.exports = function(grunt) {
grunt.config.merge({ grunt.config.merge({
nodedir: "node_modules", bowerdir: "bower_components",
copy: { copy: {
html: { html: {
options: { options: {
@ -19,21 +19,26 @@ module.exports = function(grunt) {
dest: "build/" dest: "build/"
}, },
vendorjs: { vendorjs: {
src: ["es6-shim/es6-shim.min.js", src: [ "es6-shim/es6-shim.min.js" ],
"es6-shim/es6-shim.map"],
expand: true, expand: true,
cwd: "node_modules/", cwd: "bower_components/",
dest: "build/vendor/" dest: "build/vendor/"
}, },
roboto: { robotoSlab: {
src: [ "fonts/roboto/*", src: [ "fonts/*",
"fonts/roboto-slab/*", "roboto-slab-fontface.css"
"css/roboto/roboto-fontface.css",
"css/roboto-slab/roboto-slab-fontface.css"
], ],
expand: true, expand: true,
dest: "build/", dest: "build/",
cwd: "node_modules/roboto-fontface/" cwd: "bower_components/roboto-slab-fontface"
},
roboto: {
src: [ "fonts/*",
"roboto-fontface.css"
],
expand: true,
dest: "build/",
cwd: "bower_components/roboto-fontface"
}, },
ionicons: { ionicons: {
src: [ "fonts/*", src: [ "fonts/*",
@ -41,55 +46,59 @@ module.exports = function(grunt) {
], ],
expand: true, expand: true,
dest: "build/", dest: "build/",
cwd: "node_modules/ionicons/" cwd: "bower_components/ionicons/"
}, },
leafletImages: { leafletImages: {
src: [ "images/*" ], src: [ "images/*" ],
expand: true, expand: true,
dest: "build/", dest: "build/",
cwd: "node_modules/leaflet/dist/" cwd: "bower_components/leaflet/dist/"
},
c3: {
src: ["c3.min.css"],
expand: true,
dest: "build/",
cwd: "bower_components/c3/"
} }
}, },
sass: { sass: {
options: {
sourceMap: true,
outputStyle: "compressed",
implementation: require("dart-sass")
},
dist: { dist: {
options: {
style: "compressed"
},
files: { files: {
"build/style.css": "scss/main.scss" "build/style.css": "scss/main.scss"
} }
} }
}, },
postcss: {
options: {
map: true,
processors: [
require("autoprefixer")({
browsers: ["last 2 versions"]
})
]
},
dist: {
src: "build/style.css"
}
},
cssmin: { cssmin: {
target: { target: {
files: { files: {
"build/style.css": [ "node_modules/leaflet/dist/leaflet.css", "build/style.css": [ "bower_components/leaflet/dist/leaflet.css",
"node_modules/leaflet-label/dist/leaflet.label.css", "bower_components/Leaflet.label/dist/leaflet.label.css",
"style.css" "style.css"
] ]
} }
} }
}, },
"bower-install-simple": {
options: {
directory: "<%=bowerdir%>",
color: true,
interactive: false,
production: true
},
"prod": {
options: {
production: true
}
}
},
requirejs: { requirejs: {
compile: { compile: {
options: { options: {
baseUrl: "lib", baseUrl: "lib",
name: "../node_modules/almond/almond", name: "../bower_components/almond/almond",
mainConfigFile: "app.js", mainConfigFile: "app.js",
include: "../app", include: "../app",
wrap: true, wrap: true,
@ -100,8 +109,8 @@ module.exports = function(grunt) {
} }
}) })
grunt.loadNpmTasks("grunt-bower-install-simple")
grunt.loadNpmTasks("grunt-contrib-copy") grunt.loadNpmTasks("grunt-contrib-copy")
grunt.loadNpmTasks("grunt-contrib-requirejs") grunt.loadNpmTasks("grunt-contrib-requirejs")
grunt.loadNpmTasks("grunt-sass") grunt.loadNpmTasks("grunt-contrib-sass")
grunt.loadNpmTasks("grunt-postcss")
} }

View file

@ -4,6 +4,11 @@ module.exports = function (grunt) {
options: { options: {
install: true install: true
}, },
bower: {
options: {
packageManager: "bower"
}
},
npm: {} npm: {}
}, },
eslint: { eslint: {

3451
yarn.lock

File diff suppressed because it is too large Load diff