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
node_js:
- "lts/*"
- "node"
before_install:
- gem install sass
- npm install -g grunt-cli
install:
- yarn
script: node_modules/.bin/grunt
- npm install
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.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("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 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
@ -14,32 +14,31 @@ HopGlass is a frontend for the [HopGlass Server](https://github.com/hopglass/hop
# Dependencies
- NodeJS
- yarn (recommended) or npm
- npm
- bower
- grunt-cli
- Sass (>= 3.2)
# 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)"
brew install node
brew install yarn
On Arch Linux you can install nodejs and yarn via pacman:
sudo pacman -S nodejs yarn
sudo gem install sass
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
yarn install # or `npm install`
npm install
npm install grunt-cli
# Building
@ -55,7 +54,7 @@ Copy `config.json.example` to `build/config.json` and change it to match your co
## 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!
Also, proxying the data through a webserver will allow GZip and thus will greatly reduce bandwidth consumption.
It may help with firewall problems too.
@ -120,12 +119,12 @@ Examples for `nodeInfos`:
{ "name": "Clientstatistik",
"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}"
},
{ "name": "Uptime",
"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}"
}
]

27
app.js
View file

@ -1,20 +1,21 @@
require.config({
baseUrl: "lib",
paths: {
"leaflet": "../node_modules/leaflet/dist/leaflet",
"leaflet.label": "../node_modules/leaflet-label/dist/leaflet.label",
"leaflet.providers": "../node_modules/leaflet-providers/leaflet-providers",
"chroma-js": "../node_modules/chroma-js/chroma.min",
"moment": "../node_modules/moment/min/moment-with-locales.min",
"tablesort": "../node_modules/tablesort/tablesort.min",
"tablesort.numeric": "../node_modules/tablesort/src/sorts/tablesort.numeric",
"d3": "../node_modules/d3/d3.min",
"numeral": "../node_modules/numeraljs/min/numeral.min",
"numeral-intl": "../node_modules/numeraljs/min/languages.min",
"virtual-dom": "../node_modules/virtual-dom/dist/virtual-dom",
"rbush": "../node_modules/rbush/rbush",
"leaflet": "../bower_components/leaflet/dist/leaflet",
"leaflet.label": "../bower_components/Leaflet.label/dist/leaflet.label",
"leaflet.providers": "../bower_components/leaflet-providers/leaflet-providers",
"chroma-js": "../bower_components/chroma-js/chroma.min",
"moment": "../bower_components/moment/min/moment-with-locales.min",
"tablesort": "../bower_components/tablesort/tablesort.min",
"tablesort.numeric": "../bower_components/tablesort/src/sorts/tablesort.numeric",
"d3": "../bower_components/d3/d3.min",
"numeral": "../bower_components/numeraljs/min/numeral.min",
"numeral-intl": "../bower_components/numeraljs/min/languages.min",
"virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
"rbush": "../bower_components/rbush/rbush",
"helper": "../helper",
"jshashes": "../node_modules/jshashes/hashes"
"jshashes": "../bower_components/jshashes/hashes",
"c3": "../bower_components/c3/c3.min"
},
shim: {
"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",
name: "../node_modules/almond/almond",
name: "../bower_components/almond/almond",
mainConfigFile: "app.js",
include: "../app",
wrap: true,

View file

@ -1,62 +1,62 @@
{
"dataPath": "https://map.ffdus.de/data/",
"siteName": "Freifunk Flingern",
"dataPath": [
"http://map.ffgl.eu/data/",
"http://karte.ffdus.de/data/",
"http://karte.freifunk-iserlohn.de/data/"
],
"siteName": "Freifunk Fluss",
"mapSigmaScale": 0.5,
"showContact": true,
"showContact": true,
"maxAge": 14,
"mapLayers": [
{ "name": "CartoDB",
"url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png",
{ "name": "MapQuest",
"url": "https://otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.jpg",
"config": {
"maxZoom": 18,
"attribution": "&copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>, &copy; | <a href=\"https://carto.com/attribution\">CARTO</a>"
"subdomains": "1234",
"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": "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}"
"name": "Stamen.TonerLite"
}
],
"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": [
{ "thumbnail": "https://cdn.rawgit.com/Moorviper/meshviewer_hwpics/master/nodes/{MODELHASH}.svg",
"caption": "Knoten {MODELHASH}"
"nodeCharts": [
{
"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) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest()
req.open('GET', url)
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
if (req.status == 200) {
resolve(req.response)
resolve(req.response);
}
else {
reject(Error(req.statusText))
reject(Error(req.statusText));
}
}
};
req.onerror = function() {
reject(Error("Network Error"))
}
reject(Error("Network Error"));
};
req.send()
})
req.send();
});
}
function getJSON(url) {
@ -75,8 +75,7 @@ function localStorageTest() {
function listReplace(s, subst) {
for (key in subst) {
var re = new RegExp(key, 'g')
s = s.replace(re, subst[key])
s = s.replace(key, subst[key])
}
return s
}
@ -130,13 +129,7 @@ function attributeEntry(el, label, value) {
var tr = document.createElement("tr")
var th = document.createElement("th")
if (typeof label === "string")
th.textContent = label
else {
th.appendChild(label)
tr.className = "routerpic"
}
th.textContent = label
tr.appendChild(th)
var td = document.createElement("td")
@ -155,8 +148,8 @@ function attributeEntry(el, label, value) {
function createIframe(opt, width, height) {
el = document.createElement("iframe")
width = typeof width !== 'undefined' ? width : '100%'
height = typeof height !== 'undefined' ? height : '350px'
width = typeof width !== 'undefined' ? width : '525px';
height = typeof height !== 'undefined' ? width : '350px';
if (opt.src)
el.src = opt.src
@ -186,7 +179,7 @@ function createIframe(opt, width, height) {
function showStat(o, subst) {
var content, caption
subst = typeof subst !== 'undefined' ? subst : {}
subst = typeof subst !== 'undefined' ? subst : {};
if (o.thumbnail) {
content = document.createElement("img")
@ -201,7 +194,7 @@ function showStat(o, subst) {
}
if (o.iframe) {
content = createIframe(o.iframe, o.width, o.height)
content = createIframe(o.iframe)
if (o.iframe.src)
content.src = listReplace(o.iframe.src, subst)
else

View file

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

View file

@ -3,14 +3,15 @@
<head>
<meta charset="utf-8">
<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="node_modules/roboto-fontface/css/roboto/roboto-fontface.css">
<link rel="stylesheet" href="node_modules/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="node_modules/leaflet-label/dist/leaflet.label.css">
<link rel="stylesheet" href="node_modules/ionicons/css/ionicons.min.css">
<link rel="stylesheet" href="bower_components/roboto-slab-fontface/roboto-slab-fontface.css">
<link rel="stylesheet" href="bower_components/roboto-fontface/roboto-fontface.css">
<link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.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">
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/requirejs/require.js" data-main="app"></script>
<script src="bower_components/es6-shim/es6-shim.min.js"></script>
<script src="bower_components/requirejs/require.js" data-main="app"></script>
</head>
<body>
</body>

View file

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

View file

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

View file

@ -1,7 +1,7 @@
define(["d3"], function (d3) {
var margin = 200
var NODE_RADIUS = 15
var LINE_RADIUS = 7
var LINE_RADIUS = 12
return function (config, linkScale, sidebar, router) {
var self = this
@ -214,7 +214,7 @@ define(["d3"], function (d3) {
function drawLabel(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) {
@ -242,8 +242,7 @@ define(["d3"], function (d3) {
}
function visibleLinks(d) {
return (d.o.isVPN ||
d.source.x > screenRect.left && d.source.x < screenRect.right &&
return (d.source.x > screenRect.left && d.source.x < screenRect.right &&
d.source.y > screenRect.top && d.source.y < screenRect.bottom) ||
(d.target.x > screenRect.left && d.target.x < screenRect.right &&
d.target.y > screenRect.top && d.target.y < screenRect.bottom)
@ -325,19 +324,16 @@ define(["d3"], function (d3) {
links.forEach(function (d) {
var dx = d.target.x - d.source.x
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
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.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.globalAlpha = d.o.isVPN ? 0.1 : 0.8
ctx.lineWidth = d.o.isVPN ? 1.5 : 2.5
ctx.globalAlpha = d.o.type === "VPN" ? 0.1 : 0.8
ctx.lineWidth = d.o.type === "VPN" ? 1.5 : 2.5
ctx.stroke()
})
@ -380,32 +376,31 @@ define(["d3"], function (d3) {
// -- draw clients --
ctx.save()
ctx.beginPath()
if (scale > 0.9)
nodes.filter(visibleNodes).forEach(function (d) {
var clients = d.o.node.statistics.clients
if (clients === 0)
return
nodes.filter(visibleNodes).forEach(function (d) {
var clients = d.o.node.statistics.clients
if (clients === 0)
return
var startDistance = 16
var radius = 3
var a = 1.2
var startAngle = Math.PI
var startDistance = 16
var radius = 3
var a = 1.2
var startAngle = Math.PI
for (var orbit = 0, i = 0; i < clients; orbit++) {
var distance = startDistance + orbit * 2 * radius * a
var n = Math.floor((Math.PI * distance) / (a * radius))
var delta = clients - i
for (var orbit = 0, i = 0; i < clients; orbit++) {
var distance = startDistance + orbit * 2 * radius * a
var n = Math.floor((Math.PI * distance) / (a * radius))
var delta = clients - i
for (var j = 0; j < Math.min(delta, n); i++, j++) {
var angle = 2 * Math.PI / n * j
var x = d.x + distance * Math.cos(angle + startAngle)
var y = d.y + distance * Math.sin(angle + startAngle)
for (var j = 0; j < Math.min(delta, n); i++, j++) {
var angle = 2 * Math.PI / n * j
var x = d.x + distance * Math.cos(angle + startAngle)
var y = d.y + distance * Math.sin(angle + startAngle)
ctx.moveTo(x, y)
ctx.arc(x, y, radius, 0, 2 * Math.PI)
}
ctx.moveTo(x, y)
ctx.arc(x, y, radius, 0, 2 * Math.PI)
}
})
}
})
ctx.fillStyle = clientColor
ctx.fill()
@ -475,34 +470,32 @@ define(["d3"], function (d3) {
requestAnimationFrame(redraw)
}
function distance(ax, ay, bx, by) {
return Math.pow(ax - bx, 2) + Math.pow(ay - by, 2)
function distance(a, b) {
return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)
}
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) {
/* http://stackoverflow.com/questions/849211 */
var bx = b.x - ((b.x - a.x) / 2)
var by = b.y - ((b.y - a.y) / 2)
var l2 = distance(a.x, a.y, bx, by)
var l2 = distance(a, b)
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)
return distance(p.x, p.y, a.x, a.y)
return distance(p, a)
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) {
@ -530,7 +523,7 @@ define(["d3"], function (d3) {
}
var links = intLinks.filter(function (d) {
return !d.o.isVPN
return d.o.type !== "VPN"
}).filter(function (d) {
return distanceLink(e, d.source, d.target) < LINE_RADIUS
})
@ -591,13 +584,13 @@ define(["d3"], function (d3) {
.charge(-250)
.gravity(0.1)
.linkDistance(function (d) {
if (d.o.isVPN)
if (d.o.type === "VPN")
return 0
else
return LINK_DISTANCE
})
.linkStrength(function (d) {
if (d.o.isVPN)
if (d.o.type === "VPN")
return 0
else
return Math.max(0.5, 1 / d.o.tq)
@ -651,7 +644,7 @@ define(["d3"], function (d3) {
e.source = newNodesDict[d.source.id]
e.target = newNodesDict[d.target.id]
if (d.isVPN)
if (d.type === "VPN")
e.color = "rgba(255, 255, 255, " + (0.6 / d.tq) + ")"
else
e.color = linkScale(d.tq).hex()
@ -703,15 +696,6 @@ define(["d3"], function (d3) {
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) {
d.neighbours = Object.keys(d.neighbours).map(function (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.classList.add("content")
document.body.appendChild(contentDiv)
@ -60,7 +57,7 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
contentDiv.appendChild(buttons)
var buttonToggle = document.createElement("button")
buttonToggle.textContent = "\uF133"
buttonToggle.textContent = ""
buttonToggle.onclick = function () {
if (content.constructor === Map)
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 () {
function showStatImg(o, d) {
function showStatImg(o, source, target) {
var subst = {}
subst["{SOURCE}"] = d.source.node_id
subst["{SOURCE_NAME}"] = d.source.node.nodeinfo.hostname ? d.source.node.nodeinfo.hostname : "unknown"
subst["{TARGET}"] = d.target.node_id
subst["{TARGET_NAME}"] = d.target.node.nodeinfo.hostname ? d.target.node.nodeinfo.hostname : "unknown"
subst["{SOURCE}"] = source
subst["{TARGET}"] = target
return showStat(o, subst)
}
@ -37,12 +35,15 @@ define(function () {
attributeEntry(attributes, "Hardware", (hw1 != null ? hw1 : "unbekannt") + " " + (hw2 != null ? hw2 : "unbekannt"))
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) {
var h4 = document.createElement("h4")
h4.textContent = linkInfo.name
el.appendChild(h4)
el.appendChild(showStatImg(linkInfo, d))
el.appendChild(showStatImg(linkInfo, source, target))
})
}
}
})

View file

@ -1,83 +1,80 @@
define(function () {
return function (config, el, router, d) {
var sidebarTitle = document.createElement("h2")
sidebarTitle.textContent = "Location: " + d.toString()
el.appendChild(sidebarTitle)
var h2 = document.createElement("h2")
h2.textContent = "Location: " + d.toString()
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) {
if(result.display_name)
sidebarTitle.textContent = result.display_name
h2.textContent = result.display_name
})
var editLat = document.createElement("input")
editLat.type = "text"
editLat.value = d.lat.toFixed(9)
el.appendChild(createBox("lat", "Breitengrad", editLat))
var latDiv = document.createElement("div")
var h3lat = document.createElement("h3")
h3lat.textContent = "Breitengrad"
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")
editLng.type = "text"
editLng.value = d.lng.toFixed(9)
el.appendChild(createBox("lng", "Längengrad", editLng))
var longDiv = document.createElement("div")
var h3lng = document.createElement("h3")
h3lng.textContent = "Längengrad"
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")
editUci.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"
el.appendChild(createBox("uci", "Befehl", editUci, false))
var linkPlain = document.createElement("a")
linkPlain.textContent = "plain"
linkPlain.onclick = function() {
var a1 = document.createElement("a")
a1.textContent = "plain"
a1.onclick = function() {
switch2plain()
return false
}
linkPlain.href = "#"
var linkUci = document.createElement("a")
linkUci.textContent = "uci"
linkUci.onclick = function() {
a1.href = "#"
var a2 = document.createElement("a")
a2.textContent = "uci"
a2.onclick = function() {
switch2uci()
return false
}
linkUci.href = "#"
a2.href = "#"
var hintText = document.createElement("p")
hintText.appendChild(document.createTextNode("Du kannst zwischen "))
hintText.appendChild(linkPlain)
hintText.appendChild(document.createTextNode(" und "))
hintText.appendChild(linkUci)
hintText.appendChild(document.createTextNode(" wechseln."))
el.appendChild(hintText)
var p3 = document.createElement("p")
p3.textContent = "Du kannst zwischen "
p3.appendChild(a1)
var t1 = document.createTextNode(" und ")
p3.appendChild(t1)
p3.appendChild(a2)
var t2 = document.createTextNode(" wechseln.")
p3.appendChild(t2)
el.appendChild(p3)
function createBox(name, title, inputElem, isVisible) {
var visible = typeof isVisible !== "undefined" ? isVisible : true
var box = document.createElement("div")
var heading = document.createElement("h3")
heading.textContent = title
box.appendChild(heading)
function createCopyButton(id) {
var btn = document.createElement("button")
btn.className = "ion-ios-copy"
btn.title = "Kopieren"
btn.onclick = function() { copy2clip(inputElem.id) }
inputElem.id = "location-" + name
inputElem.readOnly = true
var line = document.createElement("p")
line.appendChild(inputElem)
line.appendChild(btn)
box.appendChild(line)
box.id = "box-" + name
box.style.display = visible ? "block" : "none"
return box
btn.onclick = function() {
copy2clip(id)
}
return btn
}
function copy2clip(id) {
var copyField = document.querySelector("#" + id)
copyField.select()
var copyTextarea = document.querySelector("#" + id)
copyTextarea.select()
try {
document.execCommand("copy")
} catch (err) {
@ -86,15 +83,18 @@ define(function () {
}
function switch2plain() {
document.getElementById("box-uci").style.display = "none"
document.getElementById("box-lat").style.display = "block"
document.getElementById("box-lng").style.display = "block"
var box1 = document.getElementById("location-latitude")
box1.value = d.lat.toFixed(9)
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() {
document.getElementById("box-uci").style.display = "block"
document.getElementById("box-lat").style.display = "none"
document.getElementById("box-lng").style.display = "none"
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("h3-latitude").textContent = "Befehl"
document.getElementById("div-longitude").style.display = "none"
}
}
})

View file

@ -1,5 +1,5 @@
define(["moment", "numeral", "tablesort", "tablesort.numeric"],
function (moment, numeral, Tablesort) {
define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"],
function (moment, numeral, Tablesort, Charts) {
function showGeoURI(d) {
function showLatitude(d) {
var suffix = Math.sign(d) > -1 ? "'N" : "'S"
@ -81,124 +81,18 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
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) {
if (!d.flags.online)
return undefined
var meshclients = getMeshClients(d)
resetMeshClients(d)
var before = " ("
var after = " in der lokalen Wolke)"
return function (el) {
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"))
var span = document.createElement("span")
span.classList.add("clients")
span.textContent = " ".repeat(d.statistics.clients)
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,10 +112,7 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
if (link) {
var a = document.createElement("a")
if (ip.includes("."))
a.href = "http://" + ip + "/"
else
a.href = "http://[" + ip + "]/"
a.href = "http://[" + ip + "]/"
a.textContent = ip
el.appendChild(a)
} else
@ -265,7 +156,7 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
}
var label = document.createElement("label")
label.textContent = +(Math.round(v + "e+2") + "e-2")
label.textContent = (v)
span.appendChild(label)
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) {
var webpages = dictGet(d.nodeinfo, ["pages"])
if (webpages === null)
@ -387,37 +219,6 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
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) {
var subst = {}
subst["{NODE_ID}"] = d.nodeinfo.node_id ? d.nodeinfo.node_id : "unknown"
@ -426,30 +227,13 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
}
return function(config, el, router, d) {
var h2 = document.createElement("h2")
h2.textContent = d.nodeinfo.hostname
el.appendChild(h2)
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")
h2.textContent = d.nodeinfo.hostname
el.appendChild(h2)
}
attributeEntry(attributes, "Status", showStatus(d))
attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null)
attributeEntry(attributes, "Koordinaten", showGeoURI(d))
@ -464,20 +248,20 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
attributeEntry(attributes, "Site", showSite(d, config))
attributeEntry(attributes, "Uptime", showUptime(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, "Arbeitsspeicher", showRAM(d))
attributeEntry(attributes, "IP Adressen", showIPs(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, "Clients", showClients(d), showMeshClients(d))
attributeEntry(attributes, "Clients", showClients(d))
el.appendChild(attributes)
if (!d.flags.gateway && config.nodeCharts)
config.nodeCharts.forEach( function (config) {
el.appendChild((new Charts(d, config)).render())
})
if (config.nodeInfos)
config.nodeInfos.forEach( function (nodeInfo) {
@ -531,7 +315,13 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
tr.appendChild(td1)
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)) {
var span = document.createElement("span")

View file

@ -4,14 +4,12 @@ function (moment, Router, L, GUI, numeral) {
function handleData(data) {
var dataNodes = {}
dataNodes.nodes = []
dataNodes.nodeIds = []
var dataGraph = {}
dataGraph.batadv = {}
dataGraph.batadv.nodes = []
dataGraph.batadv.links = []
function rearrangeLinks(d) {
d.source += 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
console.log(vererr) //silent fail
} else {
data[i].nodes.forEach(fillData)
dataNodes.nodes = dataNodes.nodes.concat(data[i].nodes)
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) {
return "firstseen" in d && "lastseen" in d
})
@ -123,44 +110,17 @@ function (moment, Router, L, GUI, numeral) {
nodes.forEach( function (d) {
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) {
if (d.type === "tunnel" || d.vpn) {
if (d.type === "tunnel")
d.type = "VPN"
d.isVPN = true
} 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") {
else if (d.type === "wireless")
d.type = "Wifi"
d.isVPN = false
} else if (d.type === "other") {
else if (d.type === "other")
d.type = "Kabel"
d.isVPN = false
} else {
else
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)
if (unknown) {
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.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
})

View file

@ -18,7 +18,7 @@ define(["map/clientlayer", "map/labelslayer",
onAdd: function () {
var button = L.DomUtil.create("button", "add-layer")
button.textContent = "\uF2C7"
button.textContent = ""
// L.DomEvent.disableClickPropagation(button)
// Click propagation isn't disabled as this causes problems with the
@ -46,7 +46,7 @@ define(["map/clientlayer", "map/labelslayer",
onAdd: function () {
var button = L.DomUtil.create("button", "locate-user")
button.textContent = "\uF2E9"
button.textContent = ""
L.DomEvent.disableClickPropagation(button)
L.DomEvent.addListener(button, "click", this.onClick, this)
@ -85,7 +85,7 @@ define(["map/clientlayer", "map/labelslayer",
onAdd: function () {
var button = L.DomUtil.create("button", "coord-picker")
button.textContent = "\uF2A6"
button.textContent = ""
// Click propagation isn't disabled as this causes problems with the
// location picking mode; instead propagation is stopped in onClick().
@ -172,7 +172,7 @@ define(["map/clientlayer", "map/labelslayer",
var map, userLocation
var layerControl
var customLayers = {}
var customLayers = new Set()
var baseLayers = {}
var locateUserButton = new LocateButton(function (d) {
@ -259,16 +259,16 @@ define(["map/clientlayer", "map/labelslayer",
if (layerName in baseLayers)
return
if (layerName in customLayers)
if (customLayers.has(layerName))
return
try {
var layer = L.tileLayer.provider(layerName)
layerControl.addBaseLayer(layer, layerName)
customLayers[layerName] = layer
customLayers.add(layerName)
if (localStorageTest())
localStorage.setItem("map/customLayers", JSON.stringify(Object.keys(customLayers)))
localStorage.setItem("map/customLayers", JSON.stringify(Array.from(customLayers)))
} catch (e) {
return
}
@ -286,10 +286,16 @@ define(["map/clientlayer", "map/labelslayer",
var layers = config.mapLayers.map( function (d) {
return {
"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.forEach( function (d) {
@ -318,33 +324,16 @@ define(["map/clientlayer", "map/labelslayer",
if (d)
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.setZIndex(5)
var labelsLayer = new LabelsLayer({})
var labelsLayer = new LabelsLayer({maxZoom: maxLayerZoom})
labelsLayer.addTo(map)
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 linkDict = {}
var highlight

View file

@ -1,12 +1,15 @@
define(["leaflet"],
function (L) {
define(["leaflet", "jshashes"],
function (L, jsHashes) {
var MD5 = new jsHashes.MD5()
return L.TileLayer.Canvas.extend({
setData: function (d) {
this.data = d
//pre-calculate start angles
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()
},

View file

@ -6,22 +6,16 @@ define(function () {
self.setData = function (d) {
var totalNodes = sum(d.nodes.all.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 totalLostNodes = sum(d.nodes.lost.map(one))
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) {
return ("gateway" in d.statistics && d.statistics.gateway.id) ? d.statistics.gateway.id : d.statistics.gateway
}).concat(d.nodes.all.filter( function (d) {
var totalGateways = sum(d.nodes.all.filter(online).filter( function (d) {
return d.flags.gateway
})))).map(function(d) {
return (typeof d === "string") ? 1 : 0
}))
}).map(one))
var nodetext = [{ count: totalOnlineNodes, label: "online" },
{ count: totalOfflineNodes, label: "offline" },
{ count: totalNewNodes, label: "neu" },
{ count: totalLostNodes, label: "verschwunden" }
].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",
sort: function (a, b) {
var aname = typeof a.nodeinfo.hostname === "string" ? a.nodeinfo.hostname : a.nodeinfo.node_id
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
return a.nodeinfo.hostname.localeCompare(b.nodeinfo.hostname)
},
reverse: false
},
@ -56,7 +52,7 @@ define(["sorttable", "virtual-dom", "numeral"], function (SortTable, V, numeral)
td1Content.push(V.h("a", { className: aClass.join(" "),
onclick: router.node(d),
href: "#!n:" + d.nodeinfo.node_id
href: "#"
}, d.nodeinfo.hostname))
if (has_location(d))

View file

@ -128,13 +128,7 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
return d ? "online" : "offline"
})
var fwDict = count(nodes, ["nodeinfo", "software", "firmware", "release"])
var hwDict = count(nodes, ["nodeinfo", "hardware", "model"], function (d) {
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 hwDict = count(nodes, ["nodeinfo", "hardware", "model"])
var geoDict = count(nodes, ["nodeinfo", "location"], function (d) {
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"
})
var gwNodesDict = count(onlineNodes, ["statistics", "gateway"], function (d) {
var gwNodesDict = count(nodes, ["statistics", "gateway"], function (d) {
if (d === null)
return null
if (d.node)
return d.node.nodeinfo.hostname
if (d.id)
return d.id
if (d in nodeDict)
return nodeDict[d].nodeinfo.hostname
return d
})
@ -169,16 +160,13 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
if (d === null)
return null
if (d.node)
return d.node.nodeinfo.hostname
if (d.id)
return d.id
if (d in nodeDict)
return nodeDict[d].nodeinfo.hostname
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
if (config.siteNames)
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("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("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("Nodes an Gateway", gwNodesTable, gwNodesDict.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] }))
}

View file

@ -78,8 +78,6 @@ define(function () {
if (!s)
return false
s = decodeURIComponent(s)
if (!s.startsWith("#!"))
return false
@ -96,7 +94,7 @@ define(function () {
var id
if (args[0] === "n") {
id = args[1]
id = decodeURIComponent(args[1])
if (id in objects.nodes) {
currentObject = { node: objects.nodes[id] }
gotoNode(objects.nodes[id])
@ -105,7 +103,7 @@ define(function () {
}
if (args[0] === "l") {
id = args[1]
id = decodeURIComponent(args[1])
if (id in objects.links) {
currentObject = { link: 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(" "),
onclick: router.node(d),
href: "#!n:" + d.nodeinfo.node_id
href: "#"
}, d.nodeinfo.hostname))
if (has_location(d))

View file

@ -1,41 +1,22 @@
{
"name": "hopglass",
"version": "1.0.0",
"scripts": {
"test": "node -e \"require('grunt').cli()\" '' clean lint"
},
"devDependencies": {
"autoprefixer": "^6.3.3",
"dart-sass": "^1.16.1",
"grunt": "^1.0.3",
"grunt": "^0.4.5",
"grunt-check-dependencies": "^0.6.0",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-connect": "^0.8.0",
"grunt-contrib-copy": "^0.5.0",
"grunt-contrib-cssmin": "^0.12.2",
"grunt-contrib-requirejs": "^0.4.4",
"grunt-contrib-sass": "^0.9.2",
"grunt-contrib-uglify": "^0.5.1",
"grunt-contrib-watch": "^0.6.1",
"grunt-eslint": "^10.0.0",
"grunt-git-describe": "^2.3.2",
"grunt-postcss": "^0.7.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"
"grunt-bower-install-simple": "^1.1.2",
"grunt-git-describe": "^2.3.2"
},
"eslintConfig": {
"env": {

View file

@ -28,7 +28,3 @@ h5 {
h6 {
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,15 +2,15 @@
margin: 0;
display: flex;
flex-wrap: wrap;
font-family: Roboto, sans-serif;
font-family: Roboto;
font-size: 0.83em;
font-weight: bold;
padding: 0 6pt 6pt !important;
li {
li {
border-radius: 20pt;
display: flex;
padding: 0 0 0 8pt;
padding: 0pt 0 0pt 8pt;
margin: 3pt;
align-items: center;
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,23 +1,28 @@
.legend .symbol {
width: 1em;
height: 1em;
border-radius: 50%;
display: inline-block;
vertical-align: -5%;
.legend .symbol
{
width: 1em;
height: 1em;
border-radius: 50%;
display: inline-block;
vertical-align: -5%;
}
.legend-new .symbol {
background-color: #93E929;
.legend-new .symbol
{
background-color: #93E929;
}
.legend-online .symbol {
background-color: #1566A9;
.legend-online .symbol
{
background-color: #1566A9;
}
.legend-offline .symbol {
background-color: #D43E2A;
.legend-offline .symbol
{
background-color: #D43E2A;
}
.legend-online, .legend-offline {
margin-left: 1em;
.legend-online, .legend-offline
{
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

@ -10,7 +10,7 @@
width: 100%;
height: 100%;
.node-alert {
.node-alert {
-webkit-animation: blink 2s linear;
-webkit-animation-iteration-count: infinite;
animation: blink 2s linear;
@ -35,25 +35,13 @@
}
@-webkit-keyframes blink {
0% {
opacity: 1.0;
}
80% {
opacity: 1.0;
}
90% {
opacity: 0.0;
}
0% { opacity: 1.0; }
80% { opacity: 1.0; }
90% { opacity: 0.0; }
}
@keyframes blink {
0% {
opacity: 1.0;
}
80% {
opacity: 1.0;
}
90% {
opacity: 0.0;
}
0% { opacity: 1.0; }
80% { opacity: 1.0; }
90% { opacity: 0.0; }
}

View file

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

View file

@ -1,15 +1,19 @@
/* Original is in LESS and can be found here: https://gist.github.com/gefangenimnetz/3ef3e18364edf105c5af */
@mixin shadow($level:1) {
@mixin shadow($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);
} @else if $level == 2 {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
} @else if $level == 3 {
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
} @else if $level == 4 {
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
} @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 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
}
@else if $level == 2 {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
@else if $level == 3 {
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
}
@else if $level == 4 {
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
}
@else if $level == 5 {
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.label';
@import '_filters';
@import '_loader';
$minscreenwidth: 630pt;
$sidebarwidth: 420pt;
@ -15,6 +14,7 @@ $buttondistance: 12pt;
@import '_map';
@import '_forcegraph';
@import '_legend';
@import '_chart';
.content {
position: fixed;
@ -45,7 +45,7 @@ $buttondistance: 12pt;
margin: 0;
list-style: none;
display: flex;
font-family: Roboto, sans-serif;
font-family: Roboto;
@include shadow(1);
}
@ -70,9 +70,8 @@ $buttondistance: 12pt;
body {
margin: 0;
padding: 0;
font-family: 'Roboto-Slab', serif;
font-family: 'Roboto Slab', serif;
font-size: 11pt;
color: #333;
}
th.sort-header::selection {
@ -118,38 +117,6 @@ table.attributes td {
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 {
.infobox, .container {
@include shadow(2);
@ -177,13 +144,6 @@ table.attributes tr.routerpic th {
white-space: normal;
}
.infobox .clientsMesh {
font-family: "ionicons";
color: #dbdbdb;
word-spacing: -0.2em;
white-space: normal;
}
.infobox {
position: relative;
padding: 0.25em 0;
@ -192,27 +152,6 @@ table.attributes tr.routerpic th {
img {
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 {
@ -221,7 +160,6 @@ button {
@include shadow(1);
border-radius: 0.9em;
background: rgba(255, 255, 255, 0.7);
color: #333;
border: none;
cursor: pointer;
height: 1.8em;
@ -242,7 +180,7 @@ button:hover {
}
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 {
@ -261,8 +199,6 @@ button.close {
border-radius: 0;
color: rgba(0, 0, 0, 0.5);
font-family: "ionicons";
position: absolute;
right: 0;
&:hover {
color: #dc0067;
@ -364,7 +300,7 @@ table {
position: absolute;
top: $buttondistance;
left: $buttondistance;
padding-bottom: $buttondistance;
margin-bottom: $buttondistance;
transition: left 0.5s;
}
@ -449,9 +385,9 @@ table {
@media screen and (max-width: 80em) {
.sidebar {
font-size: 0.8em;
top: 0;
left: 0;
margin: 0;
top: 0pt;
left: 0pt;
margin: 0pt;
width: $sidebarwidthsmall;
min-height: 100vh;
@include shadow(2);
@ -482,7 +418,7 @@ table {
.sidebar {
position: static;
margin: 0 !important;
margin: 0em !important;
width: auto;
height: auto;
min-height: 0;

View file

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

View file

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

3451
yarn.lock

File diff suppressed because it is too large Load diff