From 7b57a2245b832329ef8ca7192361531809afd5e7 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Thu, 2 Apr 2015 19:59:07 +0200 Subject: [PATCH 01/11] make meshstats, simplenodelist setData idempotent --- app.js | 1 + bower.json | 3 +- lib/meshstats.js | 18 ++++++---- lib/simplenodelist.js | 76 ++++++++++++++++++++++--------------------- 4 files changed, 53 insertions(+), 45 deletions(-) diff --git a/app.js b/app.js index 1782431..aebbce3 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: { 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/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/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 From 7ddfbd964c27207638fc3bb12171a559874ba6fa Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Thu, 2 Apr 2015 20:58:30 +0200 Subject: [PATCH 02/11] make linklist.setData idempotent --- lib/linklist.js | 89 ++++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/lib/linklist.js b/lib/linklist.js index a23100e..15024dc 100644 --- a/lib/linklist.js +++ b/lib/linklist.js @@ -1,7 +1,8 @@ -define(["tablesort", "tablesort.numeric"], function (Tablesort) { +define(["tablesort", "virtual-dom", "tablesort.numeric"], + function (Tablesort, V) { return function(linkScale, router) { var self = this - var el + var el, tbody, sort self.render = function (d) { el = document.createElement("div") @@ -12,64 +13,60 @@ define(["tablesort", "tablesort.numeric"], function (Tablesort) { if (data.graph.links.length === 0) return - var h2 = document.createElement("h2") - h2.textContent = "Verbindungen" - el.appendChild(h2) + if (!tbody) { + var h2 = document.createElement("h2") + h2.textContent = "Verbindungen" + el.appendChild(h2) - var table = document.createElement("table") - var thead = document.createElement("thead") + var table = document.createElement("table") + el.appendChild(table) - var tr = document.createElement("tr") - var th1 = document.createElement("th") - th1.textContent = "Knoten" - tr.appendChild(th1) + var thead = document.createElement("thead") - var th2 = document.createElement("th") - th2.textContent = "TQ" - tr.appendChild(th2) + var tr = document.createElement("tr") + var th1 = document.createElement("th") + th1.textContent = "Knoten" + tr.appendChild(th1) - var th3 = document.createElement("th") - th3.textContent = "Entfernung" - th3.classList.add("sort-default") - tr.appendChild(th3) + var th2 = document.createElement("th") + th2.textContent = "TQ" + tr.appendChild(th2) - thead.appendChild(tr) + var th3 = document.createElement("th") + th3.textContent = "Entfernung" + th3.classList.add("sort-default") + tr.appendChild(th3) - table.appendChild(thead) + thead.appendChild(tr) + table.appendChild(thead) - var tbody = document.createElement("tbody") + tbody = document.createElement("tbody") + tbody.last = V.h("tbody") + table.appendChild(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) + sort = new Tablesort(table) + } + + var items = data.graph.links.map( function (d) { + var name = d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname + var td1Content = [V.h("a", {href: "#", onclick: router.link(d)}, name)] if (d.vpn) - td1.appendChild(document.createTextNode(" (VPN)")) + td1Content.push(" (VPN)") - var td2 = document.createElement("td") - td2.textContent = showTq(d) - td2.style.color = linkScale(d.tq) - row.appendChild(td2) + var td1 = V.h("td", td1Content) + var td2 = V.h("td", {style: {color: linkScale(d.tq)}}, showTq(d)) + var td3 = V.h("td", {attributes: { + "data-sort": d.distance !== undefined ? -d.distance : 1 + }}, showDistance(d)) - 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) + return V.h("tr", [td1, td2, td3]) }) - table.appendChild(tbody) - - new Tablesort(table) - - el.appendChild(table) + var tbodyNew = V.h("tbody", items) + tbody = V.patch(tbody, V.diff(tbody.last, tbodyNew)) + tbody.last = tbodyNew + sort.refresh() } } }) From d3bbf6060e177552e479317b2a2e1b426dc06451 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Thu, 2 Apr 2015 21:28:21 +0200 Subject: [PATCH 03/11] make nodelist.setData idempotent --- lib/nodelist.js | 103 +++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/lib/nodelist.js b/lib/nodelist.js index dabd48c..7d28905 100644 --- a/lib/nodelist.js +++ b/lib/nodelist.js @@ -1,6 +1,7 @@ -define(["tablesort", "tablesort.numeric"], function (Tablesort) { +define(["tablesort", "virtual-dom", "tablesort.numeric"], + function (Tablesort, V) { return function(router) { - function showUptime(el, now, d) { + function showUptime(now, d) { var uptime if (d.flags.online && "uptime" in d.statistics) uptime = Math.round(d.statistics.uptime / 3600) @@ -15,12 +16,11 @@ define(["tablesort", "tablesort.numeric"], function (Tablesort) { else s = uptime + "h" - el.textContent = s - el.setAttribute("data-sort", uptime !== undefined ? -uptime : 0) + return {v: s, sort: uptime !== undefined ? -uptime : 0} } var self = this - var el + var el, tbody, sort self.render = function (d) { el = document.createElement("div") @@ -31,69 +31,66 @@ define(["tablesort", "tablesort.numeric"], function (Tablesort) { if (data.nodes.all.length === 0) return - var h2 = document.createElement("h2") - h2.textContent = "Alle Knoten" - el.appendChild(h2) + if (!tbody) { + var h2 = document.createElement("h2") + h2.textContent = "Alle Knoten" + el.appendChild(h2) - var table = document.createElement("table") - var thead = document.createElement("thead") + var table = document.createElement("table") + el.appendChild(table) - var tr = document.createElement("tr") - var th1 = document.createElement("th") - th1.textContent = "Knoten" - th1.classList.add("sort-default") - tr.appendChild(th1) + var thead = document.createElement("thead") - var th2 = document.createElement("th") - th2.textContent = "Uptime" - tr.appendChild(th2) + var tr = document.createElement("tr") + var th1 = document.createElement("th") + th1.textContent = "Knoten" + th1.classList.add("sort-default") + tr.appendChild(th1) - var th3 = document.createElement("th") - th3.textContent = "Clients" - tr.appendChild(th3) + var th2 = document.createElement("th") + th2.textContent = "Uptime" + tr.appendChild(th2) - thead.appendChild(tr) + var th3 = document.createElement("th") + th3.textContent = "Clients" + tr.appendChild(th3) - table.appendChild(thead) + thead.appendChild(tr) + table.appendChild(thead) - var tbody = document.createElement("tbody") + tbody = document.createElement("tbody") + tbody.last = V.h("tbody") + table.appendChild(tbody) - data.nodes.all.forEach( function (d) { - var row = document.createElement("tr") + sort = new Tablesort(table) + } - 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 items = data.nodes.all.map( function (d) { + var td1Content = [] + var aClass = ["hostname", d.flags.online ? "online" : "offline"] - var td2 = document.createElement("td") - showUptime(td2, data.now, d) - row.appendChild(td2) + td1Content.push(V.h("a", { className: aClass.join(" "), + onclick: router.node(d), + href: "#" + }, d.nodeinfo.hostname)) - var td3 = document.createElement("td") - td3.textContent = "clients" in d.statistics ? d.statistics.clients : "" - row.appendChild(td3) + if (has_location(d)) + td1Content.push(V.h("span", {className: "icon ion-location"})) - tbody.appendChild(row) + var uptime = showUptime(data.now, d) + + var td1 = V.h("td", td1Content) + var td2 = V.h("td", {attributes: { "data-sort": uptime.sort }}, uptime.v) + var td3 = V.h("td", "clients" in d.statistics ? d.statistics.clients : "") + + return V.h("tr", [td1, td2, td3]) }) - table.appendChild(tbody) - - new Tablesort(table) - - el.appendChild(table) + var tbodyNew = V.h("tbody", items) + tbody = V.patch(tbody, V.diff(tbody.last, tbodyNew)) + tbody.last = tbodyNew + sort.refresh() } } }) From c4383aabb90489777fb630d8f2c00f0d85d65e16 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Thu, 2 Apr 2015 22:07:00 +0200 Subject: [PATCH 04/11] make proportions.setData idempotent --- app.js | 5 ++++- lib/proportions.js | 35 ++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/app.js b/app.js index aebbce3..1b8f9f9 100644 --- a/app.js +++ b/app.js @@ -18,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/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) { From 0a0b8bd065421e7060541e82184beddaff21317f Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Thu, 2 Apr 2015 22:10:11 +0200 Subject: [PATCH 05/11] removeContent: also remove from dataTargets --- lib/gui.js | 5 +++++ 1 file changed, 5 insertions(+) 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 From 0490455ccfb2dc6bf015b3a6f17050489df6fd2d Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 3 Apr 2015 00:02:00 +0200 Subject: [PATCH 06/11] handle multiple setData calls in map --- lib/map.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/map.js b/lib/map.js index 25feaef..93c442e 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") @@ -94,8 +95,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)) From d6e0587c557f75b3e7857ce8d8a4689b5b1b03a5 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 3 Apr 2015 02:32:32 +0200 Subject: [PATCH 07/11] work on forcegraph reload --- lib/forcegraph.js | 110 ++++++++++++++++++++++++++++++---------------- lib/linklist.js | 25 ++++++----- lib/main.js | 14 +++++- lib/nodelist.js | 17 ++++--- 4 files changed, 105 insertions(+), 61 deletions(-) diff --git a/lib/forcegraph.js b/lib/forcegraph.js index c6d8b03..3b926f8 100644 --- a/lib/forcegraph.js +++ b/lib/forcegraph.js @@ -1,13 +1,13 @@ 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 LINK_DISTANCE = 70 @@ -19,18 +19,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) { @@ -132,7 +132,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,25 +145,45 @@ 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 }) + link.exit().remove() + var linkEnter = link.enter().append("g") .attr("class", "link") .on("click", function (d) { @@ -175,34 +195,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,26 +231,40 @@ 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() + + if (node.enter().size() + link.enter().size() > 0) + force.start() } self.resetView = function () { @@ -245,7 +279,7 @@ define(["d3"], function (d3) { self.gotoNode = function (d) { link.classed("highlight", false) node.classed("highlight", function (e) { - return e.node === d && d !== undefined + return e.o.node === d && d !== undefined }) var n = nodesDict[d.nodeinfo.node_id] @@ -259,7 +293,7 @@ define(["d3"], function (d3) { self.gotoLink = function (d) { node.classed("highlight", false) link.classed("highlight", function (e) { - return e === d && d !== undefined + return e.o === d && d !== undefined }) var l = linksDict[d.id] diff --git a/lib/linklist.js b/lib/linklist.js index 15024dc..bdcf01a 100644 --- a/lib/linklist.js +++ b/lib/linklist.js @@ -1,8 +1,8 @@ -define(["tablesort", "virtual-dom", "tablesort.numeric"], - function (Tablesort, V) { +define(["virtual-dom"], + function (V) { return function(linkScale, router) { var self = this - var el, tbody, sort + var el, tbody self.render = function (d) { el = document.createElement("div") @@ -34,7 +34,6 @@ define(["tablesort", "virtual-dom", "tablesort.numeric"], var th3 = document.createElement("th") th3.textContent = "Entfernung" - th3.classList.add("sort-default") tr.appendChild(th3) thead.appendChild(tr) @@ -43,11 +42,16 @@ define(["tablesort", "virtual-dom", "tablesort.numeric"], tbody = document.createElement("tbody") tbody.last = V.h("tbody") table.appendChild(tbody) - - sort = new Tablesort(table) } - var items = data.graph.links.map( function (d) { + var links = data.graph.links.slice(0).sort( function (a, b) { + a = a.distance === undefined ? -1 : a.distance + b = b.distance === undefined ? -1 : b.distance + + return b - a + }) + + var items = links.map( function (d) { var name = d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname var td1Content = [V.h("a", {href: "#", onclick: router.link(d)}, name)] @@ -55,10 +59,8 @@ define(["tablesort", "virtual-dom", "tablesort.numeric"], td1Content.push(" (VPN)") var td1 = V.h("td", td1Content) - var td2 = V.h("td", {style: {color: linkScale(d.tq)}}, showTq(d)) - var td3 = V.h("td", {attributes: { - "data-sort": d.distance !== undefined ? -d.distance : 1 - }}, showDistance(d)) + 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]) }) @@ -66,7 +68,6 @@ define(["tablesort", "virtual-dom", "tablesort.numeric"], var tbodyNew = V.h("tbody", items) tbody = V.patch(tbody, V.diff(tbody.last, tbodyNew)) tbody.last = tbodyNew - sort.refresh() } } }) 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/nodelist.js b/lib/nodelist.js index 7d28905..93ef47b 100644 --- a/lib/nodelist.js +++ b/lib/nodelist.js @@ -1,5 +1,5 @@ -define(["tablesort", "virtual-dom", "tablesort.numeric"], - function (Tablesort, V) { +define(["virtual-dom"], + function (V) { return function(router) { function showUptime(now, d) { var uptime @@ -20,7 +20,7 @@ define(["tablesort", "virtual-dom", "tablesort.numeric"], } var self = this - var el, tbody, sort + var el, tbody self.render = function (d) { el = document.createElement("div") @@ -44,7 +44,6 @@ define(["tablesort", "virtual-dom", "tablesort.numeric"], 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") @@ -61,12 +60,13 @@ define(["tablesort", "virtual-dom", "tablesort.numeric"], tbody = document.createElement("tbody") tbody.last = V.h("tbody") table.appendChild(tbody) - - sort = new Tablesort(table) } + var nodes = data.nodes.all.slice(0).sort( function (a, b) { + return a.nodeinfo.hostname.localeCompare(b.nodeinfo.hostname) + }) - var items = data.nodes.all.map( function (d) { + var items = nodes.map( function (d) { var td1Content = [] var aClass = ["hostname", d.flags.online ? "online" : "offline"] @@ -81,7 +81,7 @@ define(["tablesort", "virtual-dom", "tablesort.numeric"], var uptime = showUptime(data.now, d) var td1 = V.h("td", td1Content) - var td2 = V.h("td", {attributes: { "data-sort": uptime.sort }}, uptime.v) + var td2 = V.h("td", uptime.v) var td3 = V.h("td", "clients" in d.statistics ? d.statistics.clients : "") return V.h("tr", [td1, td2, td3]) @@ -90,7 +90,6 @@ define(["tablesort", "virtual-dom", "tablesort.numeric"], var tbodyNew = V.h("tbody", items) tbody = V.patch(tbody, V.diff(tbody.last, tbodyNew)) tbody.last = tbodyNew - sort.refresh() } } }) From d9084fa462b9a6d909ca87d68c0765325e752a0b Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Mon, 6 Apr 2015 11:37:52 +0200 Subject: [PATCH 08/11] forcegraph: keep objects highlighted on updates --- lib/forcegraph.js | 79 ++++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/lib/forcegraph.js b/lib/forcegraph.js index 3b926f8..0e1dc74 100644 --- a/lib/forcegraph.js +++ b/lib/forcegraph.js @@ -8,6 +8,7 @@ define(["d3"], function (d3) { var el var doAnimation = false var intNodes = [] + var highlight var LINK_DISTANCE = 70 @@ -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[linkId(highlight.o)] + + 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 }) @@ -263,47 +306,27 @@ define(["d3"], function (d3) { .links(intLinks) .size([diameter, diameter]) + 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.o.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.o === 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 } From 290bc6f3597bf69c475a80598d1118464a0a388c Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Mon, 6 Apr 2015 11:58:02 +0200 Subject: [PATCH 09/11] map: keep objects highlighted on updates --- lib/map.js | 126 +++++++++++++++++++++++++++++------------------------ 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/lib/map.js b/lib/map.js index 93c442e..cac1d95 100644 --- a/lib/map.js +++ b/lib/map.js @@ -74,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[linkId(highlight.o)] + + 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 }) @@ -135,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 () { From aae9d4253e209489bcc02f51a34d15f189146c7a Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Mon, 6 Apr 2015 23:14:24 +0200 Subject: [PATCH 10/11] fixes --- lib/forcegraph.js | 4 ++-- lib/map.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/forcegraph.js b/lib/forcegraph.js index 0e1dc74..4ba279c 100644 --- a/lib/forcegraph.js +++ b/lib/forcegraph.js @@ -115,7 +115,7 @@ define(["d3"], function (d3) { return } else if (highlight.type === "link") { - var l = linksDict[linkId(highlight.o)] + var l = linksDict[highlight.o.id] if (l) { node.classed("highlight", false) @@ -223,7 +223,7 @@ define(["d3"], function (d3) { 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() diff --git a/lib/map.js b/lib/map.js index cac1d95..8f5f05b 100644 --- a/lib/map.js +++ b/lib/map.js @@ -118,7 +118,7 @@ define(["d3", "leaflet", "moment", "leaflet.label"], function (d3, L, moment) { if (m) m.setStyle({ fillColor: m.options.color, color: "orange", weight: 20, fillOpacity: 1, opacity: 0.7 }) } else if (highlight.type === "link") { - m = linkDict[linkId(highlight.o)] + m = linkDict[highlight.o.id] if (m) m.setStyle({ weight: 7, opacity: 1, dashArray: "10, 10" }) From 88bc4aafc5624bcf2950281592b961337c6004fd Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Tue, 7 Apr 2015 17:41:17 +0200 Subject: [PATCH 11/11] nodelist/linklist: sortable tables --- lib/linklist.js | 111 +++++++++++++++-------------------- lib/nodelist.js | 149 +++++++++++++++++++++-------------------------- lib/sorttable.js | 57 ++++++++++++++++++ tasks/linting.js | 3 +- 4 files changed, 174 insertions(+), 146 deletions(-) create mode 100644 lib/sorttable.js diff --git a/lib/linklist.js b/lib/linklist.js index bdcf01a..1484721 100644 --- a/lib/linklist.js +++ b/lib/linklist.js @@ -1,73 +1,56 @@ -define(["virtual-dom"], - function (V) { - return function(linkScale, router) { - var self = this - var el, tbody +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) - if (!tbody) { - var h2 = document.createElement("h2") - h2.textContent = "Verbindungen" - el.appendChild(h2) + var h2 = document.createElement("h2") + h2.textContent = "Verbindungen" + el.appendChild(h2) - var table = document.createElement("table") - el.appendChild(table) + el.appendChild(table.el) + } - var thead = document.createElement("thead") - - 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" - tr.appendChild(th3) - - thead.appendChild(tr) - table.appendChild(thead) - - tbody = document.createElement("tbody") - tbody.last = V.h("tbody") - table.appendChild(tbody) - } - - var links = data.graph.links.slice(0).sort( function (a, b) { - a = a.distance === undefined ? -1 : a.distance - b = b.distance === undefined ? -1 : b.distance - - return b - a - }) - - var items = links.map( function (d) { - var name = d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname - var td1Content = [V.h("a", {href: "#", onclick: router.link(d)}, name)] - - 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]) - }) - - var tbodyNew = V.h("tbody", items) - tbody = V.patch(tbody, V.diff(tbody.last, tbodyNew)) - tbody.last = tbodyNew + this.setData = function (d) { + table.setData(d.graph.links) } } }) diff --git a/lib/nodelist.js b/lib/nodelist.js index 93ef47b..1ab4113 100644 --- a/lib/nodelist.js +++ b/lib/nodelist.js @@ -1,95 +1,82 @@ -define(["virtual-dom"], - function (V) { +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(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"})) - return {v: s, 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, tbody + 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) + + var h2 = document.createElement("h2") + h2.textContent = "Alle Knoten" + el.appendChild(h2) + + el.appendChild(table.el) } - self.setData = function (data) { - if (data.nodes.all.length === 0) - return - - if (!tbody) { - var h2 = document.createElement("h2") - h2.textContent = "Alle Knoten" - el.appendChild(h2) - - var table = document.createElement("table") - el.appendChild(table) - - var thead = document.createElement("thead") - - var tr = document.createElement("tr") - var th1 = document.createElement("th") - th1.textContent = "Knoten" - 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) - - tbody = document.createElement("tbody") - tbody.last = V.h("tbody") - table.appendChild(tbody) - } - - var nodes = data.nodes.all.slice(0).sort( function (a, b) { - return a.nodeinfo.hostname.localeCompare(b.nodeinfo.hostname) + 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 }) - var items = nodes.map( function (d) { - var td1Content = [] - var aClass = ["hostname", d.flags.online ? "online" : "offline"] - - td1Content.push(V.h("a", { className: aClass.join(" "), - onclick: router.node(d), - href: "#" - }, d.nodeinfo.hostname)) - - if (has_location(d)) - td1Content.push(V.h("span", {className: "icon ion-location"})) - - var uptime = showUptime(data.now, d) - - var td1 = V.h("td", td1Content) - var td2 = V.h("td", uptime.v) - var td3 = V.h("td", "clients" in d.statistics ? d.statistics.clients : "") - - return V.h("tr", [td1, td2, td3]) - }) - - var tbodyNew = V.h("tbody", items) - tbody = V.patch(tbody, V.diff(tbody.last, tbodyNew)) - tbody.last = tbodyNew - } + table.setData(data) + } } }) 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: {