diff --git a/app.js b/app.js index 1782431..1b8f9f9 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,7 @@ require.config({ "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", "helper": "../helper" }, shim: { @@ -17,7 +18,10 @@ require.config({ "tablesort": { exports: "Tablesort" }, - "numeral-intl": ["numeral"], + "numeral-intl": { + deps: ["numeral"], + exports: "numeral" + }, "tablesort.numeric": ["tablesort"], "helper": ["numeral-intl"] } diff --git a/bower.json b/bower.json index c15b14a..595bc34 100644 --- a/bower.json +++ b/bower.json @@ -21,7 +21,8 @@ "r.js": "~2.1.16", "d3": "~3.5.5", "numeraljs": "~1.5.3", - "roboto-fontface": "~0.3.0" + "roboto-fontface": "~0.3.0", + "virtual-dom": "~2.0.1" }, "authors": [ "Nils Schneider " diff --git a/lib/forcegraph.js b/lib/forcegraph.js index c6d8b03..4ba279c 100644 --- a/lib/forcegraph.js +++ b/lib/forcegraph.js @@ -1,13 +1,14 @@ define(["d3"], function (d3) { return function (config, linkScale, sidebar, router) { var self = this - var nodes, links var svg, vis, link, node var nodesDict, linksDict var zoomBehavior var force var el var doAnimation = false + var intNodes = [] + var highlight var LINK_DISTANCE = 70 @@ -19,18 +20,18 @@ define(["d3"], function (d3) { if (!localStorageTest()) return - var save = nodes.map( function (d) { - return { id: d.id, x: d.x, y: d.y } + var save = intNodes.map( function (d) { + return { id: d.o.id, x: d.x, y: d.y } }) localStorage.setItem("graph/nodeposition", JSON.stringify(save)) } function nodeName(d) { - if (d.node && d.node.nodeinfo) - return d.node.nodeinfo.hostname + if (d.o.node && d.o.node.nodeinfo) + return d.o.node.nodeinfo.hostname else - return d.id + return d.o.id } function dragstart(d) { @@ -97,6 +98,48 @@ define(["d3"], function (d3) { animatePanzoom(translate, scale) } + function updateHighlight(nopanzoom) { + if (highlight !== undefined) + if (highlight.type === "node") { + var n = nodesDict[highlight.o.nodeinfo.node_id] + + if (n) { + link.classed("highlight", false) + node.classed("highlight", function (e) { + return e.o.node === n.o.node && n.o.node !== undefined + }) + + if (!nopanzoom) + panzoomTo([n.x, n.y], [n.x, n.y]) + } + + return + } else if (highlight.type === "link") { + var l = linksDict[highlight.o.id] + + if (l) { + node.classed("highlight", false) + link.classed("highlight", function (e) { + return e.o === l.o && l.o !== undefined + }) + + if (!nopanzoom) { + var x = d3.extent([l.source, l.target], function (d) { return d.x }) + var y = d3.extent([l.source, l.target], function (d) { return d.y }) + panzoomTo([x[0], y[0]], [x[1], y[1]]) + } + } + + return + } + + node.classed("highlight", false) + link.classed("highlight", false) + + if (!nopanzoom) + panzoomTo([0, 0], force.size()) + } + function tickEvent() { link.selectAll("line") .attr("x1", function(d) { return d.source.x }) @@ -132,7 +175,7 @@ define(["d3"], function (d3) { .gravity(0.05) .linkDistance(LINK_DISTANCE) .linkStrength(function (d) { - return 1 / d.tq + return 1 / d.o.tq }) .on("tick", tickEvent) .on("end", savePositions) @@ -145,24 +188,44 @@ define(["d3"], function (d3) { .on("dragend", dragend) self.setData = function (data) { - var nodePositions = {} + var oldNodes = {} - if (localStorageTest()) { - var save = JSON.parse(localStorage.getItem("graph/nodeposition")) + intNodes.forEach( function (d) { + oldNodes[d.o.id] = d + }) - if (save) - save.forEach( function (d) { - nodePositions[d.id] = d - }) - } + intNodes = data.graph.nodes.map( function (d) { + var e + if (d.id in oldNodes) + e = oldNodes[d.id] + else + e = {} - links = data.graph.links.filter( function (d) { + e.o = d + + return e + }) + + var newNodesDict = {} + + intNodes.forEach( function (d) { + newNodesDict[d.o.id] = d + }) + + var intLinks = data.graph.links.filter( function (d) { return !d.vpn + }).map( function (d) { + var source = newNodesDict[d.source.id] + var target = newNodesDict[d.target.id] + + return {o: d, source: source, target: target} }) link = vis.select("g.links") .selectAll("g.link") - .data(links, function (d) { return d.id }) + .data(intLinks, function (d) { return d.o.id }) + + link.exit().remove() var linkEnter = link.enter().append("g") .attr("class", "link") @@ -175,34 +238,34 @@ define(["d3"], function (d3) { .append("title") link.selectAll("line") - .style("stroke", function (d) { return linkScale(d.tq) }) + .style("stroke", function (d) { return linkScale(d.o.tq).hex() }) - link.selectAll("title").text(showTq) + link.selectAll("title").text(function (d) { return showTq(d.o) }) linksDict = {} link.each( function (d) { - if (d.source.node && d.target.node) - linksDict[d.id] = d + if (d.o.source.node && d.o.target.node) + linksDict[d.o.id] = d }) - nodes = data.graph.nodes - node = vis.select("g.nodes") .selectAll(".node") - .data(nodes, function(d) { return d.id }) + .data(intNodes, function(d) { return d.o.id }) + + node.exit().remove() var nodeEnter = node.enter().append("circle") .attr("r", 8) .on("click", function (d) { if (!d3.event.defaultPrevented) - router.node(d.node)() + router.node(d.o.node)() }) .call(draggableNode) node.attr("class", function (d) { var s = ["node"] - if (!d.node) + if (!d.o.node) s.push("unknown") return s.join(" ") @@ -211,65 +274,59 @@ define(["d3"], function (d3) { nodesDict = {} node.each( function (d) { - if (d.node) - nodesDict[d.node.nodeinfo.node_id] = d + if (d.o.node) + nodesDict[d.o.node.nodeinfo.node_id] = d }) nodeEnter.append("title") - nodeEnter.each( function (d) { - if (nodePositions[d.id]) { - d.x = nodePositions[d.id].x - d.y = nodePositions[d.id].y + + if (localStorageTest()) { + var save = JSON.parse(localStorage.getItem("graph/nodeposition")) + + if (save) { + var nodePositions = {} + save.forEach( function (d) { + nodePositions[d.id] = d + }) + + nodeEnter.each( function (d) { + if (nodePositions[d.o.id]) { + d.x = nodePositions[d.o.id].x + d.y = nodePositions[d.o.id].y + } + }) } - }) + } node.selectAll("title").text(nodeName) - var diameter = graphDiameter(nodes) + var diameter = graphDiameter(intNodes) - force.nodes(nodes) - .links(links) + force.nodes(intNodes) + .links(intLinks) .size([diameter, diameter]) - .start() + + updateHighlight(true) + + if (node.enter().size() + link.enter().size() > 0) + force.start() } self.resetView = function () { - node.classed("highlight", false) - link.classed("highlight", false) - - panzoomTo([0, 0], force.size()) - + highlight = undefined + updateHighlight() doAnimation = true } self.gotoNode = function (d) { - link.classed("highlight", false) - node.classed("highlight", function (e) { - return e.node === d && d !== undefined - }) - - var n = nodesDict[d.nodeinfo.node_id] - - if (n) - panzoomTo([n.x, n.y], [n.x, n.y]) - + highlight = {type: "node", o: d} + updateHighlight() doAnimation = true } self.gotoLink = function (d) { - node.classed("highlight", false) - link.classed("highlight", function (e) { - return e === d && d !== undefined - }) - - var l = linksDict[d.id] - - if (l) { - var x = d3.extent([l.source, l.target], function (d) { return d.x }) - var y = d3.extent([l.source, l.target], function (d) { return d.y }) - panzoomTo([x[0], y[0]], [x[1], y[1]]) - } - + highlight = {type: "link", o: d} + updateHighlight() doAnimation = true } diff --git a/lib/gui.js b/lib/gui.js index fd05def..b859e05 100644 --- a/lib/gui.js +++ b/lib/gui.js @@ -13,11 +13,16 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Linklist, var linkScale = chroma.scale(chroma.interpolate.bezier(["green", "yellow", "red"])).domain([1, 5]) var sidebar + function dataTargetRemove(d) { + dataTargets = dataTargets.filter( function (e) { return d !== e }) + } + function removeContent() { if (!content) return router.removeTarget(content) + dataTargetRemove(content) content.destroy() contentDiv.removeChild(content.div) content = null diff --git a/lib/linklist.js b/lib/linklist.js index a23100e..1484721 100644 --- a/lib/linklist.js +++ b/lib/linklist.js @@ -1,75 +1,56 @@ -define(["tablesort", "tablesort.numeric"], function (Tablesort) { - return function(linkScale, router) { - var self = this - var el +define(["sorttable", "virtual-dom"], function (SortTable, V) { + function linkName(d) { + return d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname + } - self.render = function (d) { - el = document.createElement("div") - d.appendChild(el) + var headings = [{ name: "Knoten", + sort: function (a, b) { + return linkName(a).localeCompare(linkName(b)) + }, + reverse: false + }, + { name: "TQ", + sort: function (a, b) { return a.tq - b.tq}, + reverse: true + }, + { name: "Entfernung", + sort: function (a, b) { + return (a.distance === undefined ? -1 : a.distance) - + (b.distance === undefined ? -1 : b.distance) + }, + reverse: true + }] + + return function(linkScale, router) { + var table = new SortTable(headings, 2, renderRow) + + function renderRow(d) { + var td1Content = [V.h("a", {href: "#", onclick: router.link(d)}, linkName(d))] + + if (d.vpn) + td1Content.push(" (VPN)") + + var td1 = V.h("td", td1Content) + var td2 = V.h("td", {style: {color: linkScale(d.tq).hex()}}, showTq(d)) + var td3 = V.h("td", showDistance(d)) + + return V.h("tr", [td1, td2, td3]) } - self.setData = function (data) { - if (data.graph.links.length === 0) - return + this.render = function (d) { + var el = document.createElement("div") + el.last = V.h("div") + d.appendChild(el) var h2 = document.createElement("h2") h2.textContent = "Verbindungen" el.appendChild(h2) - var table = document.createElement("table") - var thead = document.createElement("thead") + el.appendChild(table.el) + } - var tr = document.createElement("tr") - var th1 = document.createElement("th") - th1.textContent = "Knoten" - tr.appendChild(th1) - - var th2 = document.createElement("th") - th2.textContent = "TQ" - tr.appendChild(th2) - - var th3 = document.createElement("th") - th3.textContent = "Entfernung" - th3.classList.add("sort-default") - tr.appendChild(th3) - - thead.appendChild(tr) - - table.appendChild(thead) - - var tbody = document.createElement("tbody") - - data.graph.links.forEach( function (d) { - var row = document.createElement("tr") - var td1 = document.createElement("td") - var a = document.createElement("a") - a.textContent = d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname - a.href = "#" - a.onclick = router.link(d) - td1.appendChild(a) - row.appendChild(td1) - - if (d.vpn) - td1.appendChild(document.createTextNode(" (VPN)")) - - var td2 = document.createElement("td") - td2.textContent = showTq(d) - td2.style.color = linkScale(d.tq) - row.appendChild(td2) - - var td3 = document.createElement("td") - td3.textContent = showDistance(d) - td3.setAttribute("data-sort", d.distance !== undefined ? -d.distance : 1) - row.appendChild(td3) - - tbody.appendChild(row) - }) - - table.appendChild(tbody) - - new Tablesort(table) - - el.appendChild(table) + this.setData = function (d) { + table.setData(d.graph.links) } } }) diff --git a/lib/main.js b/lib/main.js index 83411e4..941b297 100644 --- a/lib/main.js +++ b/lib/main.js @@ -88,14 +88,24 @@ function (config, moment, Router, L, GUI, numeral) { var urls = [ config.dataPath + "nodes.json", config.dataPath + "graph.json" ] + function update() { + return Promise.all(urls.map(getJSON)) + .then(handleData) + } - Promise.all(urls.map(getJSON)) - .then(handleData) + update() .then(function (d) { var gui = new GUI(config, router) gui.setData(d) router.setData(d) router.start() + + window.setInterval(function () { + update().then(function (d) { + gui.setData(d) + router.setData(d) + }) + }, 60000) }) .catch(function (e) { console.log(e) diff --git a/lib/map.js b/lib/map.js index 25feaef..8f5f05b 100644 --- a/lib/map.js +++ b/lib/map.js @@ -57,6 +57,7 @@ define(["d3", "leaflet", "moment", "leaflet.label"], function (d3, L, moment) { return function (config, linkScale, sidebar, router) { var self = this var barycenter + var groupOnline, groupOffline, groupNew, groupLost, groupLines var el = document.createElement("div") el.classList.add("map") @@ -73,6 +74,62 @@ define(["d3", "leaflet", "moment", "leaflet.label"], function (d3, L, moment) { var nodeDict = {} var linkDict = {} + var highlight + + function resetMarkerStyles(nodes, links) { + Object.keys(nodes).forEach( function (d) { + nodes[d].resetStyle() + }) + + Object.keys(links).forEach( function (d) { + links[d].resetStyle() + }) + } + + function setView(bounds) { + map.fitBounds(bounds, {paddingTopLeft: [sidebar.getWidth(), 0]}) + } + + function resetZoom() { + setView(barycenter.getBounds()) + } + + function goto(m) { + var bounds + + if ("getBounds" in m) + bounds = m.getBounds() + else + bounds = L.latLngBounds([m.getLatLng()]) + + setView(bounds) + + return m + } + + function updateView(nopanzoom) { + resetMarkerStyles(nodeDict, linkDict) + var m + + if (highlight !== undefined) + if (highlight.type === "node") { + m = nodeDict[highlight.o.nodeinfo.node_id] + + if (m) + m.setStyle({ fillColor: m.options.color, color: "orange", weight: 20, fillOpacity: 1, opacity: 0.7 }) + } else if (highlight.type === "link") { + m = linkDict[highlight.o.id] + + if (m) + m.setStyle({ weight: 7, opacity: 1, dashArray: "10, 10" }) + } + + if (!nopanzoom) + if (m) + goto(m) + else + resetZoom() + } function calcBarycenter(nodes) { nodes = nodes.map(function (d) { return d.nodeinfo.location }) @@ -94,8 +151,23 @@ define(["d3", "leaflet", "moment", "leaflet.label"], function (d3, L, moment) { nodeDict = {} linkDict = {} + if (groupOffline) + groupOffline.clearLayers() + + if (groupOnline) + groupOnline.clearLayers() + + if (groupNew) + groupNew.clearLayers() + + if (groupLost) + groupLost.clearLayers() + + if (groupLines) + groupLines.clearLayers() + var lines = addLinksToMap(linkDict, linkScale, data.graph.links, router) - L.featureGroup(lines).addTo(map) + groupLines = L.featureGroup(lines).addTo(map) barycenter = calcBarycenter(data.nodes.all.filter(has_location)) @@ -119,71 +191,27 @@ define(["d3", "leaflet", "moment", "leaflet.label"], function (d3, L, moment) { return iconLost }, router)) - L.featureGroup(markersOffline).addTo(map) - L.featureGroup(markersOnline).addTo(map) - L.featureGroup(markersNew).addTo(map) - L.featureGroup(markersLost).addTo(map) + groupOffline = L.featureGroup(markersOffline).addTo(map) + groupOnline = L.featureGroup(markersOnline).addTo(map) + groupNew = L.featureGroup(markersNew).addTo(map) + groupLost = L.featureGroup(markersLost).addTo(map) + + updateView(true) } - function resetMarkerStyles(nodes, links) { - Object.keys(nodes).forEach( function (d) { - nodes[d].resetStyle() - }) - - Object.keys(links).forEach( function (d) { - links[d].resetStyle() - }) + self.resetView = function () { + highlight = undefined + updateView() } - function setView(bounds) { - map.fitBounds(bounds, {paddingTopLeft: [sidebar.getWidth(), 0]}) - } - - function resetView() { - resetMarkerStyles(nodeDict, linkDict) - - setView(barycenter.getBounds()) - } - - function goto(dict, id) { - var m = dict[id] - if (m === undefined) - return undefined - - var bounds - - if ("getBounds" in m) - bounds = m.getBounds() - else - bounds = L.latLngBounds([m.getLatLng()]) - - setView(bounds) - - return m - } - - self.resetView = resetView - self.gotoNode = function (d) { - resetMarkerStyles(nodeDict, linkDict) - - var m = goto(nodeDict, d.nodeinfo.node_id) - - if (m) - m.setStyle({ fillColor: m.options.color, color: "orange", weight: 20, fillOpacity: 1, opacity: 0.7 }) - else - resetView() + highlight = {type: "node", o: d} + updateView() } self.gotoLink = function (d) { - resetMarkerStyles(nodeDict, linkDict) - - var m = goto(linkDict, d.id) - - if (m) - m.setStyle({ weight: 7, opacity: 1, dashArray: "10, 10" }) - else - resetView() + highlight = {type: "link", o: d} + updateView() } self.destroy = function () { diff --git a/lib/meshstats.js b/lib/meshstats.js index 3f4b195..c378ffa 100644 --- a/lib/meshstats.js +++ b/lib/meshstats.js @@ -1,7 +1,7 @@ define(function () { return function () { var self = this - var p + var stats, timestamp self.setData = function (d) { var totalNodes = sum(d.nodes.all.filter(online).map(one)) @@ -12,12 +12,11 @@ define(function () { return d.flags.gateway }).map(one)) - p.textContent = totalNodes + " Knoten (online), " + - totalClients + " Clients, " + - totalGateways + " Gateways" + stats.textContent = totalNodes + " Knoten (online), " + + totalClients + " Clients, " + + totalGateways + " Gateways" - p.appendChild(document.createElement("br")) - p.appendChild(document.createTextNode("Diese Daten sind von " + d.timestamp.format("LLLL") + ".")) + timestamp.textContent = "Diese Daten sind von " + d.timestamp.format("LLLL") + "." } self.render = function (el) { @@ -25,8 +24,13 @@ define(function () { h2.textContent = "Übersicht" el.appendChild(h2) - p = document.createElement("p") + var p = document.createElement("p") el.appendChild(p) + stats = document.createTextNode("") + p.appendChild(stats) + p.appendChild(document.createElement("br")) + timestamp = document.createTextNode("") + p.appendChild(timestamp) } return self diff --git a/lib/nodelist.js b/lib/nodelist.js index dabd48c..1ab4113 100644 --- a/lib/nodelist.js +++ b/lib/nodelist.js @@ -1,99 +1,82 @@ -define(["tablesort", "tablesort.numeric"], function (Tablesort) { +define(["sorttable", "virtual-dom", "numeral"], function (SortTable, V, numeral) { + function getUptime(now, d) { + if (d.flags.online && "uptime" in d.statistics) + return Math.round(d.statistics.uptime / 3600) + else if (!d.flags.online && "lastseen" in d) + return Math.round(-(now - d.lastseen) / 3600000) + } + + function showUptime(uptime) { + var s = "" + + if (uptime !== undefined) + if (Math.abs(uptime) >= 24) + s = Math.round(uptime / 24) + "d" + else + s = uptime + "h" + + return s + } + + var headings = [{ name: "Knoten", + sort: function (a, b) { + return a.nodeinfo.hostname.localeCompare(b.nodeinfo.hostname) + }, + reverse: false + }, + { name: "Uptime", + sort: function (a, b) { return a.uptime - b.uptime}, + reverse: true + }, + { name: "Clients", + sort: function (a, b) { + return ("clients" in a.statistics ? a.statistics.clients : -1) - + ("clients" in b.statistics ? b.statistics.clients : -1) + }, + reverse: true + }] + return function(router) { - function showUptime(el, now, d) { - var uptime - if (d.flags.online && "uptime" in d.statistics) - uptime = Math.round(d.statistics.uptime / 3600) - else if (!d.flags.online && "lastseen" in d) - uptime = Math.round(-(now - d.lastseen) / 3600000) + function renderRow(d) { + var td1Content = [] + var aClass = ["hostname", d.flags.online ? "online" : "offline"] - var s = "" + td1Content.push(V.h("a", { className: aClass.join(" "), + onclick: router.node(d), + href: "#" + }, d.nodeinfo.hostname)) - if (uptime !== undefined) - if (Math.abs(uptime) >= 24) - s = Math.round(uptime / 24) + "d" - else - s = uptime + "h" + if (has_location(d)) + td1Content.push(V.h("span", {className: "icon ion-location"})) - el.textContent = s - el.setAttribute("data-sort", uptime !== undefined ? -uptime : 0) + var td1 = V.h("td", td1Content) + var td2 = V.h("td", showUptime(d.uptime)) + var td3 = V.h("td", numeral("clients" in d.statistics ? d.statistics.clients : "").format("0,0")) + + return V.h("tr", [td1, td2, td3]) } - var self = this - var el + var table = new SortTable(headings, 0, renderRow) - self.render = function (d) { - el = document.createElement("div") + this.render = function (d) { + var el = document.createElement("div") d.appendChild(el) - } - - self.setData = function (data) { - if (data.nodes.all.length === 0) - return var h2 = document.createElement("h2") h2.textContent = "Alle Knoten" el.appendChild(h2) - var table = document.createElement("table") - var thead = document.createElement("thead") + el.appendChild(table.el) + } - var tr = document.createElement("tr") - var th1 = document.createElement("th") - th1.textContent = "Knoten" - th1.classList.add("sort-default") - tr.appendChild(th1) - - var th2 = document.createElement("th") - th2.textContent = "Uptime" - tr.appendChild(th2) - - var th3 = document.createElement("th") - th3.textContent = "Clients" - tr.appendChild(th3) - - thead.appendChild(tr) - - table.appendChild(thead) - - var tbody = document.createElement("tbody") - - data.nodes.all.forEach( function (d) { - var row = document.createElement("tr") - - var td1 = document.createElement("td") - var a = document.createElement("a") - a.textContent = d.nodeinfo.hostname - a.href = "#" - a.onclick = router.node(d) - a.classList.add("hostname") - a.classList.add(d.flags.online ? "online" : "offline") - td1.appendChild(a) - row.appendChild(td1) - - if (has_location(d)) { - var span = document.createElement("span") - span.classList.add("icon") - span.classList.add("ion-location") - td1.appendChild(span) - } - - var td2 = document.createElement("td") - showUptime(td2, data.now, d) - row.appendChild(td2) - - var td3 = document.createElement("td") - td3.textContent = "clients" in d.statistics ? d.statistics.clients : "" - row.appendChild(td3) - - tbody.appendChild(row) + this.setData = function (d) { + var data = d.nodes.all.map(function (e) { + var n = Object.create(e) + n.uptime = getUptime(d.now, e) + return n }) - table.appendChild(tbody) - - new Tablesort(table) - - el.appendChild(table) - } + table.setData(data) + } } }) diff --git a/lib/proportions.js b/lib/proportions.js index 569f2f6..a0040c6 100644 --- a/lib/proportions.js +++ b/lib/proportions.js @@ -1,4 +1,6 @@ -define(["chroma-js"], function (Chroma) { +define(["chroma-js", "virtual-dom", "numeral-intl"], + function (Chroma, V, numeral) { + return function () { var self = this var fwTable, hwTable, autoTable, gwTable @@ -23,30 +25,33 @@ define(["chroma-js"], function (Chroma) { } function fillTable(table, data) { + if (!table.last) + table.last = V.h("table") + var max = 0 data.forEach(function (d) { if (d[1] > max) max = d[1] }) - data.forEach(function (d) { + var items = data.map(function (d) { var v = d[1] / max - var row = document.createElement("tr") - var th = document.createElement("th") - var td = document.createElement("td") - var span = document.createElement("span") - th.textContent = d[0] - span.style.width = Math.round(v * 100) + "%" - span.style.backgroundColor = scale(v).hex() var c1 = Chroma.contrast(scale(v), "white") var c2 = Chroma.contrast(scale(v), "black") - span.style.color = c1 > c2 ? "white" : "black" - span.textContent = d[1] - td.appendChild(span) - row.appendChild(th) - row.appendChild(td) - table.appendChild(row) + + var th = V.h("th", d[0]) + var td = V.h("td", V.h("span", {style: { + width: Math.round(v * 100) + "%", + backgroundColor: scale(v).hex(), + color: c1 > c2 ? "white" : "black" + }}, numeral(d[1]).format("0,0"))) + + return V.h("tr", [th, td]) }) + + var tableNew = V.h("table", items) + table = V.patch(table, V.diff(table.last, tableNew)) + table.last = tableNew } self.setData = function (data) { diff --git a/lib/simplenodelist.js b/lib/simplenodelist.js index ce9d307..972bc6c 100644 --- a/lib/simplenodelist.js +++ b/lib/simplenodelist.js @@ -1,7 +1,7 @@ -define(["moment"], function (moment) { +define(["moment", "virtual-dom"], function (moment, V) { return function(config, nodes, field, router, title) { var self = this - var el + var el, tbody self.render = function (d) { el = document.createElement("div") @@ -11,52 +11,54 @@ define(["moment"], function (moment) { self.setData = function (data) { var list = data.nodes[nodes] - if (list.length === 0) + if (list.length === 0) { + while (el.firstChild) + el.removeChild(el.firstChild) + + tbody = null + return + } - var h2 = document.createElement("h2") - h2.textContent = title - el.appendChild(h2) - var table = document.createElement("table") - el.appendChild(table) + if (!tbody) { + var h2 = document.createElement("h2") + h2.textContent = title + el.appendChild(h2) - var tbody = document.createElement("tbody") + var table = document.createElement("table") + el.appendChild(table) - list.forEach( function (d) { - var time = moment(d[field]).fromNow() + tbody = document.createElement("tbody") + tbody.last = V.h("tbody") + table.appendChild(tbody) + } - var row = document.createElement("tr") - var td1 = document.createElement("td") - var a = document.createElement("a") - a.classList.add("hostname") - a.classList.add(d.flags.online ? "online" : "offline") - a.textContent = d.nodeinfo.hostname - a.href = "#" - a.onclick = router.node(d) - td1.appendChild(a) + var items = list.map( function (d) { + var time = moment(d[field]).from(data.now) + var td1Content = [] - if (has_location(d)) { - var span = document.createElement("span") - span.classList.add("icon") - span.classList.add("ion-location") - td1.appendChild(span) - } + var aClass = ["hostname", d.flags.online ? "online" : "offline"] - if ("owner" in d.nodeinfo && config.showContact) { - var contact = d.nodeinfo.owner.contact - td1.appendChild(document.createTextNode(" – " + contact + "")) - } + td1Content.push(V.h("a", { className: aClass.join(" "), + onclick: router.node(d), + href: "#" + }, d.nodeinfo.hostname)) - var td2 = document.createElement("td") - td2.textContent = time + if (has_location(d)) + td1Content.push(V.h("span", {className: "icon ion-location"})) - row.appendChild(td1) - row.appendChild(td2) - tbody.appendChild(row) + if ("owner" in d.nodeinfo && config.showContact) + td1Content.push(" - " + d.nodeinfo.owner.contact) + + var td1 = V.h("td", td1Content) + var td2 = V.h("td", time) + + return V.h("tr", [td1, td2]) }) - table.appendChild(tbody) - el.appendChild(table) + var tbodyNew = V.h("tbody", items) + tbody = V.patch(tbody, V.diff(tbody.last, tbodyNew)) + tbody.last = tbodyNew } return self diff --git a/lib/sorttable.js b/lib/sorttable.js new file mode 100644 index 0000000..d6a39a1 --- /dev/null +++ b/lib/sorttable.js @@ -0,0 +1,57 @@ +define(["virtual-dom"], function (V) { + return function(headings, sortIndex, renderRow) { + var data + var sortReverse = false + var el = document.createElement("table") + var elLast = V.h("table") + + function sortTable(i) { + sortReverse = i === sortIndex ? !sortReverse : false + sortIndex = i + + updateView() + } + + function sortTableHandler(i) { + return function () { sortTable(i) } + } + + function updateView() { + var children = [] + + if (data.length !== 0) { + var th = headings.map(function (d, i) { + var properties = { onclick: sortTableHandler(i), + className: "sort-header" + } + + if (sortIndex === i) + properties.className += sortReverse ? " sort-up" : " sort-down" + + return V.h("th", properties, d.name) + }) + + var links = data.slice(0).sort(headings[sortIndex].sort) + + if (headings[sortIndex].reverse ? !sortReverse : sortReverse) + links = links.reverse() + + children.push(V.h("thead", V.h("tr", th))) + children.push(V.h("tbody", links.map(renderRow))) + } + + var elNew = V.h("table", children) + el = V.patch(el, V.diff(elLast, elNew)) + elLast = elNew + } + + this.setData = function (d) { + data = d + updateView() + } + + this.el = el + + return this + } +}) diff --git a/tasks/linting.js b/tasks/linting.js index 974029d..8355fde 100644 --- a/tasks/linting.js +++ b/tasks/linting.js @@ -19,7 +19,8 @@ module.exports = function (grunt) { "strict": [2, "never"], "no-multi-spaces": 0, "no-new": 0, - "no-shadow": 0 + "no-shadow": 0, + "no-use-before-define": [1, "nofunc"] } }, sources: {