Browse Source

Merge remote-tracking branch 'meshviewer/filter'

Michael Schwarz 8 years ago
parent
commit
766c0c08ef
13 changed files with 455 additions and 48 deletions
  1. 0 3
      .travis.yml
  2. 2 2
      lib/about.js
  3. 80 0
      lib/datadistributor.js
  4. 40 0
      lib/filters/filtergui.js
  5. 52 0
      lib/filters/genericnode.js
  6. 59 0
      lib/filters/hostname.js
  7. 33 0
      lib/filters/nodefilter.js
  8. 24 26
      lib/gui.js
  9. 9 6
      lib/main.js
  10. 23 11
      lib/proportions.js
  11. 60 0
      lib/vercomp.js
  12. 72 0
      scss/_filters.scss
  13. 1 0
      scss/main.scss

+ 0 - 3
.travis.yml

@@ -2,9 +2,6 @@ language: node_js
 before_install:
   - gem install sass
   - npm install -g grunt-cli
-  - npm install -g bower
 install:
   - npm install
-  - bower install
-  - cp config.js.example config.js
 script: grunt

+ 2 - 2
lib/about.js

@@ -28,8 +28,8 @@ define(function () {
       s += "https://www.gnu.org/licenses/</a>.</p>"
 
       s += "<p>You may find the source code at "
-      s += "<a href=\"http://draic.info/meshviewer\">"
-      s += "http://draic.info/meshviewer</a>."
+      s += "<a href=\"https://git.c3pb.de/freifunk-pb/map-website\">"
+      s += "https://git.c3pb.de/freifunk-pb/map-website</a>."
 
       el.innerHTML = s
     }

+ 80 - 0
lib/datadistributor.js

@@ -0,0 +1,80 @@
+define(["filters/nodefilter"], function (NodeFilter) {
+  return function () {
+    var targets = []
+    var filterObservers = []
+    var filters = []
+    var filteredData
+    var data
+
+    function remove(d) {
+      targets = targets.filter( function (e) { return d !== e } )
+    }
+
+    function add(d) {
+      targets.push(d)
+
+      if (filteredData !== undefined)
+        d.setData(filteredData)
+    }
+
+    function setData(d) {
+      data = d
+      refresh()
+    }
+
+    function refresh() {
+      if (data === undefined)
+        return
+
+      var filter = filters.reduce( function (a, f) {
+        return function (d) {
+          return a(d) && f.run(d)
+        }
+      }, function () { return true })
+
+      filteredData = new NodeFilter(filter)(data)
+
+      targets.forEach( function (t) {
+        t.setData(filteredData)
+      })
+    }
+
+    function notifyObservers() {
+      filterObservers.forEach( function (d) {
+        d.filtersChanged(filters)
+      })
+    }
+
+    function addFilter(d) {
+      filters.push(d)
+      notifyObservers()
+      d.setRefresh(refresh)
+      refresh()
+    }
+
+    function removeFilter(d) {
+      filters = filters.filter( function (e) { return d !== e } )
+      notifyObservers()
+      refresh()
+    }
+
+    function watchFilters(d) {
+      filterObservers.push(d)
+
+      d.filtersChanged(filters)
+
+      return function () {
+        filterObservers = filterObservers.filter( function (e) { return d !== e })
+      }
+    }
+
+    return { add: add,
+             remove: remove,
+             setData: setData,
+             addFilter: addFilter,
+             removeFilter: removeFilter,
+             watchFilters: watchFilters,
+             refresh: refresh
+           }
+  }
+})

+ 40 - 0
lib/filters/filtergui.js

@@ -0,0 +1,40 @@
+define([], function () {
+  return function (distributor) {
+    var container = document.createElement("ul")
+    container.classList.add("filters")
+    var div = document.createElement("div")
+
+    function render(el) {
+      el.appendChild(div)
+    }
+
+    function filtersChanged(filters) {
+      while (container.firstChild)
+        container.removeChild(container.firstChild)
+
+      filters.forEach( function (d) {
+        var li = document.createElement("li")
+        var div = document.createElement("div")
+        container.appendChild(li)
+        li.appendChild(div)
+        d.render(div)
+
+        var button = document.createElement("button")
+        button.textContent = ""
+        button.onclick = function () {
+          distributor.removeFilter(d)
+        }
+        li.appendChild(button)
+      })
+
+      if (container.parentNode === div && filters.length === 0)
+        div.removeChild(container)
+      else if (filters.length > 0)
+        div.appendChild(container)
+    }
+
+    return { render: render,
+             filtersChanged: filtersChanged
+           }
+  }
+})

+ 52 - 0
lib/filters/genericnode.js

@@ -0,0 +1,52 @@
+define([], function () {
+  return function (name, key, value, f) {
+    var negate = false
+    var refresh
+
+    var label = document.createElement("label")
+    var strong = document.createElement("strong")
+    label.textContent = name + " "
+    label.appendChild(strong)
+
+    function run(d) {
+      var o = dictGet(d, key.slice(0))
+
+      if (f)
+        o = f(o)
+
+      return o === value ? !negate : negate
+    }
+
+    function setRefresh(f) {
+      refresh = f
+    }
+
+    function draw(el) {
+      if (negate)
+        el.parentNode.classList.add("not")
+      else
+        el.parentNode.classList.remove("not")
+
+      strong.textContent = (negate ? "¬" : "" ) + value
+    }
+
+    function render(el) {
+      el.appendChild(label)
+      draw(el)
+
+      label.onclick = function () {
+        negate = !negate
+
+        draw(el)
+
+        if (refresh)
+          refresh()
+      }
+    }
+
+    return { run: run,
+             setRefresh: setRefresh,
+             render: render
+           }
+  }
+})

+ 59 - 0
lib/filters/hostname.js

@@ -0,0 +1,59 @@
+define([], function () {
+  return function () {
+    var refreshFunction
+    var input = document.createElement("input")
+    input.addEventListener("input", function () {
+      if (refreshFunction)
+        refreshFunction()
+    })
+
+    var value = document.createElement("strong")
+    value.classList.add("input")
+
+    updateValue()
+
+    function updateValue() {
+      value.textContent = input.value
+    }
+
+    function run(d) {
+      return d.nodeinfo.hostname.toLowerCase().includes(input.value.toLowerCase())
+    }
+
+    function setRefresh(f) {
+      refreshFunction = f
+    }
+
+    function render(el) {
+      var label = document.createElement("label")
+      label.textContent = "Hostname"
+      el.appendChild(label)
+      el.appendChild(document.createTextNode(" "))
+      el.appendChild(value)
+
+      el.onclick = function () {
+        el.removeChild(value)
+        el.appendChild(input)
+        input.focus()
+      }
+
+      input.onblur = blur
+      input.onkeypress = function (e) {
+        if (e.keyCode === 13)
+          input.blur()
+      }
+
+      function blur() {
+        updateValue()
+
+        el.removeChild(input)
+        el.appendChild(value)
+      }
+    }
+
+    return { run: run,
+             setRefresh: setRefresh,
+             render: render
+           }
+  }
+})

+ 33 - 0
lib/filters/nodefilter.js

@@ -0,0 +1,33 @@
+define([], function () {
+  return function (filter) {
+    return function (data) {
+      var n = Object.create(data)
+      n.nodes = {}
+
+      for (var key in data.nodes) {
+        n.nodes[key] = data.nodes[key].filter(filter)
+      }
+
+      var filteredIds = new Set()
+
+      n.graph = {}
+      n.graph.nodes = data.graph.nodes.filter( function (d) {
+        if (!d.node)
+          return true
+
+        var r = filter(d.node)
+
+        if (r)
+          filteredIds.add(d.id)
+
+        return r
+      })
+
+      n.graph.links = data.graph.links.filter( function (d) {
+        return filteredIds.has(d.source.id) && filteredIds.has(d.target.id)
+      })
+
+      return n
+    }
+  }
+})

+ 24 - 26
lib/gui.js

@@ -1,13 +1,12 @@
 define([ "chroma-js", "map", "sidebar", "tabs", "container", "meshstats",
          "legend", "linklist", "nodelist", "simplenodelist", "infobox/main",
-         "proportions", "forcegraph", "title", "about" ],
+         "proportions", "forcegraph", "title", "about", "datadistributor",
+         "filters/filtergui", "filters/hostname" ],
 function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
           Nodelist, SimpleNodelist, Infobox, Proportions, ForceGraph,
-          Title, About) {
+          Title, About, DataDistributor, FilterGUI, HostnameFilter) {
   return function (config, router) {
     var self = this
-    var dataTargets = []
-    var latestData
     var content
     var contentDiv
 
@@ -17,16 +16,17 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
     var buttons = document.createElement("div")
     buttons.classList.add("buttons")
 
-    function dataTargetRemove(d) {
-      dataTargets = dataTargets.filter( function (e) { return d !== e })
-    }
+    var fanout = new DataDistributor()
+    var fanoutUnfiltered = new DataDistributor()
+    fanoutUnfiltered.add(fanout)
 
     function removeContent() {
       if (!content)
         return
 
       router.removeTarget(content)
-      dataTargetRemove(content)
+      fanout.remove(content)
+
       content.destroy()
 
       content = null
@@ -38,10 +38,7 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
       content = new K(config, linkScale, sidebar.getWidth, router, buttons)
       content.render(contentDiv)
 
-      if (latestData)
-        content.setData(latestData)
-
-      dataTargets.push(content)
+      fanout.add(content)
       router.addTarget(content)
     }
 
@@ -82,15 +79,15 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
     var lostnodeslist = new SimpleNodelist("lost", "lastseen", router, "Verschwundene Knoten")
     var nodelist = new Nodelist(router)
     var linklist = new Linklist(linkScale, router)
-    var statistics = new Proportions(config)
+    var statistics = new Proportions(config, fanout)
     var about = new About()
 
-    dataTargets.push(meshstats)
-    dataTargets.push(newnodeslist)
-    dataTargets.push(lostnodeslist)
-    dataTargets.push(nodelist)
-    dataTargets.push(linklist)
-    dataTargets.push(statistics)
+    fanoutUnfiltered.add(meshstats)
+    fanoutUnfiltered.add(newnodeslist)
+    fanoutUnfiltered.add(lostnodeslist)
+    fanout.add(nodelist)
+    fanout.add(linklist)
+    fanout.add(statistics)
 
     sidebar.add(header)
     header.add(meshstats)
@@ -99,6 +96,13 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
     overview.add(newnodeslist)
     overview.add(lostnodeslist)
 
+    var filterGUI = new FilterGUI(fanout)
+    fanout.watchFilters(filterGUI)
+    header.add(filterGUI)
+
+    var hostnameFilter = new HostnameFilter()
+    fanout.addFilter(hostnameFilter)
+
     sidebar.add(tabs)
     tabs.add("Aktuelles", overview)
     tabs.add("Knoten", nodelist)
@@ -114,13 +118,7 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
 
     router.view("m")
 
-    self.setData = function (data) {
-      latestData = data
-
-      dataTargets.forEach(function (d) {
-        d.setData(data)
-      })
-    }
+    self.setData = fanoutUnfiltered.setData
 
     return self
   }

+ 9 - 6
lib/main.js

@@ -5,14 +5,12 @@ function (moment, Router, L, GUI, numeral) {
       var dataNodes = data[0]
       var dataGraph = data[1]
 
-      if (dataNodes.version !== 1 || dataGraph.version !== 1) {
+      if (dataNodes.version !== 2 || dataGraph.version !== 1) {
         var err = "Unsupported nodes or graph version: " + dataNodes.version + " " + dataGraph.version
         throw err
       }
 
-      var nodes = Object.keys(dataNodes.nodes).map(function (key) { return dataNodes.nodes[key] })
-
-      nodes = nodes.filter( function (d) {
+      var nodes = dataNodes.nodes.filter( function (d) {
         return "firstseen" in d && "lastseen" in d
       })
 
@@ -27,7 +25,12 @@ function (moment, Router, L, GUI, numeral) {
       var newnodes = limit("firstseen", age, sortByKey("firstseen", nodes).filter(online))
       var lostnodes = limit("lastseen", age, sortByKey("lastseen", nodes).filter(offline))
 
-      var graphnodes = dataNodes.nodes
+      var graphnodes = {}
+
+      dataNodes.nodes.forEach( function (d) {
+        graphnodes[d.nodeinfo.node_id] = d
+      })
+
       var graph = dataGraph.batadv
 
       graph.nodes.forEach( function (d) {
@@ -75,7 +78,7 @@ function (moment, Router, L, GUI, numeral) {
       })
 
       return { now: now,
-               timestamp: moment.utc(data[0].timestamp).local(),
+               timestamp: moment.utc(dataNodes.timestamp).local(),
                nodes: {
                  all: nodes,
                  new: newnodes,

+ 23 - 11
lib/proportions.js

@@ -1,7 +1,7 @@
-define(["chroma-js", "virtual-dom", "numeral-intl"],
-  function (Chroma, V, numeral) {
+define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "vercomp" ],
+  function (Chroma, V, numeral, Filter, vercomp) {
 
-  return function (config) {
+  return function (config, filterManager) {
     var self = this
     var scale = Chroma.scale("YlGnBu").mode("lab")
 
@@ -68,10 +68,18 @@ define(["chroma-js", "virtual-dom", "numeral-intl"],
         dict[v] = 1 + (v in dict ? dict[v] : 0)
       })
 
-      return Object.keys(dict).map(function (d) { return [d, dict[d]] })
+      return Object.keys(dict).map(function (d) { return [d, dict[d], key, f] })
     }
 
-    function fillTable(table, data) {
+    function addFilter(filter) {
+      return function () {
+        filterManager.addFilter(filter)
+
+        return false
+      }
+    }
+
+    function fillTable(name, table, data) {
       if (!table.last)
         table.last = V.h("table")
 
@@ -86,7 +94,11 @@ define(["chroma-js", "virtual-dom", "numeral-intl"],
         var c1 = Chroma.contrast(scale(v), "white")
         var c2 = Chroma.contrast(scale(v), "black")
 
-        var th = V.h("th", d[0])
+        var filter = new Filter(name, d[2], d[0], d[3])
+
+        var a = V.h("a", { href: "#", onclick: addFilter(filter) }, d[0])
+
+        var th = V.h("th", a)
         var td = V.h("td", V.h("span", {style: {
                                          width: Math.round(v * 100) + "%",
                                          backgroundColor: scale(v).hex(),
@@ -127,11 +139,11 @@ define(["chroma-js", "virtual-dom", "numeral-intl"],
           return "(deaktiviert)"
       })
 
-      fillTable(statusTable, statusDict.sort(function (a, b) { return b[1] - a[1] }))
-      fillTable(fwTable, fwDict.sort(function (a, b) { return b[1] - a[1] }))
-      fillTable(hwTable, hwDict.sort(function (a, b) { return b[1] - a[1] }))
-      fillTable(geoTable, geoDict.sort(function (a, b) { return b[1] - a[1] }))
-      fillTable(autoTable, autoDict.sort(function (a, b) { return b[1] - a[1] }))
+      fillTable("Status", statusTable, statusDict.sort(function (a, b) { return b[1] - a[1] }))
+      fillTable("Firmware", fwTable, fwDict.sort(function (a, b) { return vercomp(b[0], a[0]) }))
+      fillTable("Hardware", hwTable, hwDict.sort(function (a, b) { return b[1] - a[1] }))
+      fillTable("Koordinaten", geoTable, geoDict.sort(function (a, b) { return b[1] - a[1] }))
+      fillTable("Autom. Updates", autoTable, autoDict.sort(function (a, b) { return b[1] - a[1] }))
     }
 
     self.render = function (el) {

+ 60 - 0
lib/vercomp.js

@@ -0,0 +1,60 @@
+define([], function () {
+  function order(c) {
+    if (/^\d$/.test(c))
+      return 0
+    else if (/^[a-z]$/i.test(c))
+      return c.charCodeAt(0)
+    else if (c === "~")
+      return -1
+    else if (c)
+      return c.charCodeAt(0) + 256
+    else
+      return 0
+  }
+
+  // Based on dpkg code
+  function vercomp(a, b) {
+    var apos = 0, bpos = 0
+    while (apos < a.length || bpos < b.length) {
+      var firstDiff = 0
+
+      while ((apos < a.length && !/^\d$/.test(a[apos])) || (bpos < b.length && !/^\d$/.test(b[bpos]))) {
+        var ac = order(a[apos])
+        var bc = order(b[bpos])
+
+        if (ac !== bc)
+          return ac - bc
+
+        apos++
+        bpos++
+      }
+
+      while (a[apos] === "0")
+        apos++
+
+      while (b[bpos] === "0")
+        bpos++
+
+      while (/^\d$/.test(a[apos]) && /^\d$/.test(b[bpos])) {
+        if (firstDiff === 0)
+          firstDiff = a.charCodeAt(apos) - b.charCodeAt(bpos)
+
+        apos++
+        bpos++
+      }
+
+      if (/^\d$/.test(a[apos]))
+        return 1
+
+      if (/^\d$/.test(b[bpos]))
+        return -1
+
+      if (firstDiff !== 0)
+        return firstDiff
+    }
+
+    return 0
+  }
+
+  return vercomp
+})

+ 72 - 0
scss/_filters.scss

@@ -0,0 +1,72 @@
+.filters {
+  margin: 0;
+  display: flex;
+  flex-wrap: wrap;
+  font-family: Roboto;
+  font-size: 0.83em;
+  font-weight: bold;
+  padding: 0 6pt 6pt !important;
+
+  li  {
+    border-radius: 20pt;
+    display: flex;
+    padding: 0pt 0 0pt 8pt;
+    margin: 3pt;
+    align-items: center;
+    background: #009ee0;
+    color: rgba(255, 255, 255, 0.8);
+
+    label {
+      cursor: pointer;
+    }
+
+    strong {
+      color: rgba(255, 255, 255, 1);
+    }
+
+    &.not {
+      background: #dc0067;
+    }
+
+    button {
+      box-shadow: none;
+      margin: 2pt;
+      padding: 0;
+      width: 18pt;
+      height: 18pt;
+      background: rgba(255, 255, 255, 0.0);
+      font-size: 1.41em;
+      vertical-align: middle;
+      color: rgba(255, 255, 255, 0.8);
+
+      &:hover {
+        box-shadow: none !important;
+        color: #dc0067;
+        background: rgba(255, 255, 255, 0.9);
+      }
+
+      &:active {
+        box-shadow: none;
+      }
+    }
+  }
+
+  input {
+    background: transparent;
+    border-bottom: 2pt solid rgba(255, 255, 255, 0.5);
+    padding: 2pt 0;
+    outline: 0;
+    color: #fff;
+    font-weight: bold;
+    font-size: 1em;
+    border: none;
+
+    &:focus {
+      background: rgba(255, 255, 255, 0.15);
+    }
+  }
+
+  strong.input {
+    cursor: text;
+  }
+}

+ 1 - 0
scss/main.scss

@@ -3,6 +3,7 @@
 @import '_base';
 @import '_leaflet';
 @import '_leaflet.label';
+@import '_filters';
 
 $minscreenwidth: 630pt;
 $sidebarwidth: 420pt;