Browse Source

interactive viewer

subDesTagesMitExtraKaese 4 years ago
parent
commit
03697eba9c
3 changed files with 271 additions and 78 deletions
  1. 15 7
      channel-statistics.js
  2. 193 48
      ts3-viewer/index.php
  3. 63 23
      ts3-viewer/stylesheet.css

+ 15 - 7
channel-statistics.js

@@ -35,13 +35,21 @@ registerPlugin({
   const backend = require('backend');
   const event = require('event');
   const helpers = require('helpers');
-  const dbc = db.connect({ driver: 'mysql', host: config.host, username: config.username, password: config.password, database: config.database }, function(err) {
-      if (err) {
-           engine.log(err);
-      } else {
-        engine.log('connection successful');
-      }
-  });
+  let dbc = null; 
+
+  function reconnect() {
+    if(!dbc) {
+      dbc = db.connect({ driver: 'mysql', host: config.host, username: config.username, password: config.password, database: config.database }, function(err) {
+        if (err) {
+             engine.log(err);
+        } else {
+          engine.log('connection successful');
+        }
+      });
+    }
+  }
+  reconnect();
+  setInterval(reconnect, 1000 * 3600);
   
   let channelIdMap = {};
   let serverId = null;

+ 193 - 48
ts3-viewer/index.php

@@ -10,73 +10,144 @@
 		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.min.js"></script>
 		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.stack.min.js"></script>
 		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.time.min.js"></script>
-		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.selection.min.js"></script>
+		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.navigate.min.js"></script>
 		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.resize.min.js"></script>
 
     </head>
 	<body>
-    <header class="main-head">TS3 channel usage</header>
-    <div id="tree"></div>
-    <div id="graphs"></div>
+    <div class="wrapper">
+      <header class="main-head">TS3 channel usage</header>
+      <div id="graphs"></div>
+      <div id="settings">
+        <input type=button onclick="updateDate(addDays(startDate, -1))" value="-1 Day">
+        <input type="date" onchange="updateDate(this.valueAsDate)" id="date">
+        <input type=button onclick="updateDate(addDays(startDate, 1))" value="+1 Day">
+      </div>
+      <div id="tree"></div>
+    </div>
     <script>
-    function update() {
+    var startDate, endDate;
+    var eventTimeRange = [0, 0];
+
+    function updateDate(newDate) {
+      newDate.setHours(6, 0, 0, 0);
+      if(newDate.getTime() > eventTimeRange[1]) 
+        newDate = new Date(eventTimeRange[1]);
+      if(newDate.getTime() < eventTimeRange[0])
+        newDate = new Date(eventTimeRange[0]);
+
+      newDate.setHours(6, 0, 0, 0);
+      startDate = newDate;
+      endDate = addDays(startDate, 1);
+      document.getElementById('date').valueAsDate = startDate;
+      updatePlots();
+    }
+    
+    
+    function update(init = false) {
       fetch("ajax.php").then(res=>res.json()).then(function(json) {
-        
-        let channelTree = {channelId: 0, name: 'Server'};
+        console.log(json);
+        let channelTree = {channelId: 0, name: 'Server', id: 0};
+        eventTimeRange[0] = json.events.reduce((min, e) => min && (min < e[1]) ? min : e[1]) * 1000 - 12*3600000;
+        eventTimeRange[1] = json.events.reduce((max, e) => max && (max > e[1]) ? max : e[1]) * 1000 + 12*3600000;
+        var dateInp = document.getElementById('date');
+        dateInp.setAttribute('min', (new Date(eventTimeRange[0])).toISOString().split("T")[0]);
+        dateInp.setAttribute('max', (new Date(eventTimeRange[1])).toISOString().split("T")[0]);
+
+        if(init) 
+          updateDate(new Date());
         makeTree(channelTree, json.channels, json.events);
         showTree(channelTree, document.getElementById('tree'));
 
-        let div = $("#graphs")[0];
-        div.innerHTML = "";
-        for(const channel of channelTree.children) {
-          if(channel.clientCount == 0)
-            continue;
-
-          let container = document.createElement("div");
-          container.classList.add("demo-container");
-          div.appendChild(container);
+        if(init) {
+          for(const channel of channelTree.children) {
+            if(channel.activeCount == 0)
+              continue;
+            let inp = document.getElementById(`cb_${channel.channelId}`);
+            inp.checked = true;
 
-          let placeholder = document.createElement("div");
-          placeholder.classList.add("demo-placeholder");
-          container.appendChild(placeholder);
-
-          plotEvents(channel, placeholder);
+            plotEvents(channel, document.getElementById("graphs"), true);
+          }
         }
-
         console.log(channelTree);
       });
     }
-    update();
+    update(true);
     setInterval(update, 30 * 1000);
 
     function makeTree(tree, channels, events) {
       tree.events = events.filter(e => e[0] == tree.id);
+      if(tree.events.length > 0) {
+        tree.events.splice(0,0,[tree.events[0][0], (new Date()).getTime()/1000, tree.events[0][2]]);
+      }
       tree.children = channels.filter((ch) => ch.parentId == tree.channelId).sort((a, b) => a.position - b.position);
       tree.clientCount = tree.events.reduce((sum, e) => sum+e[2], 0) / (tree.events.length > 0 ? tree.events.length : 1);
-
+      tree.activeCount = tree.events.length > 0 ? tree.events[0][2] : 0;
       for(const child of tree.children) {
-        tree.clientCount += makeTree(child, channels.filter( ch => ch.parentId != tree.channelId), events.filter( e => e[0] != tree.id));
+        makeTree(child, channels.filter( ch => ch.parentId != tree.channelId), events.filter( e => e[0] != tree.id));
+        tree.clientCount += child.clientCount;
+        tree.activeCount += child.activeCount;
       }
-      return tree.clientCount;
     }
-
     function showTree(tree, el) {
-      el.innerHTML = `${formatHeadings(tree.name)}`;
-      if(tree.events.length > 0 && (tree.events.length != 1 || tree.events[0][2] != 0))
-        for(const event of tree.events) {
-          el.innerHTML += `<br>${(new Date(event[1]*1000)).toLocaleTimeString('de-DE', {timeZone: 'Europe/Berlin'})}: ${event[2]}`
-        }
-      let ul = document.createElement('ul');
-      el.appendChild(ul);
+      let ul = document.getElementById(`ul_${tree.channelId}`);
+      if(!ul) {
+        let inp = document.createElement('input');
+        inp.type = 'checkbox';
+        inp.id = `cb_${tree.channelId}`;
+        inp.classList.add('checkbox');
+        el.appendChild(inp);
+
+        let cnt = document.createElement('label');
+        cnt.classList.add("count");
+        cnt.htmlFor = `cb_${tree.channelId}`;
+        el.appendChild(cnt);
+        
+        let label = document.createElement('label');
+        label.htmlFor = `cb_${tree.channelId}`;
+        el.appendChild(label);
+
+        ul = document.createElement('ul');
+        ul.id = `ul_${tree.channelId}`;
+        el.appendChild(ul);
+      }
+      let inp = document.getElementById(`cb_${tree.channelId}`);
+      inp.onchange = function() {
+        plotEvents(tree, document.getElementById("graphs"), this.checked);
+      };
+      if(inp.checked) {
+        plotEvents(tree, document.getElementById("graphs"), true);
+      }
+
+      if(tree.events.length > 0)
+        inp.nextSibling.textContent = tree.events[0][2] != 0 ? tree.events[0][2] : '';
+
+      inp.nextSibling.nextSibling.innerHTML = formatHeadings(tree.name);
+
+      let prevLi = {};
+      let showSpacer = false;
       for(const channel of tree.children) {
-        if(channel.clientCount == 0)
+        if(channel.clientCount == 0) {
+          showSpacer = formatHeadings(channel.name, false).length == 1;
           continue;
-        let li = document.createElement('li');
-        ul.appendChild(li);
+        }
+        let li = document.getElementById(`li_${channel.channelId}`);
+        if(!li) {
+          li = document.createElement('li');
+          li.id = `li_${channel.channelId}`;
+          if(prevLi.nextSibling) 
+            ul.insertBefore(li, prevLi.nextSibling);
+          else
+            ul.appendChild(li);
+        }
+        if(showSpacer) {
+          li.classList.add("spacer");
+          showSpacer = false;
+        }
+        prevLi = li;
         showTree(channel, li);
       }
     }
-
     function formatHeadings(str, html = true) {
       const res = /(?:\[(.)spacer[^\]]*\])?(.*)/.exec(str);
       if(!html)
@@ -94,13 +165,35 @@
           return `<div class='channel'>${res[2]}</div>`;
       }
     }
-    
-    function plotEvents(tree, div) {
+
+    var plots = {};
+    var viewMin = 0;
+    var viewMax = 0;
+    function plotEvents(tree, div, visible = true) {
+      let placeholder = document.getElementById(`graph_${tree.channelId}`);
+      if(!placeholder) {
+
+        container = document.createElement("div");
+        container.classList.add("demo-container");
+        div.appendChild(container);
+
+        placeholder = document.createElement("div");
+        placeholder.classList.add("demo-placeholder");
+        placeholder.id = `graph_${tree.channelId}`;
+        container.appendChild(placeholder);
+      }
+
+      if(!document.getElementById(`cb_${tree.channelId}`).checked) {
+        placeholder.parentNode.style.display = 'none';
+        return;
+      } else {
+        placeholder.parentNode.style.display = 'block';
+      }
       let series = [];
       function iterChilds(tree, prefix="") {
         const data = tree.events.map(e => [e[1]*1000, e[2]]);
         const name = prefix + formatHeadings(tree.name, false);
-        if(data.length > 0 && (data.length != 1 || data[0][1] != 0)) {
+        if(data.length > 0 && (data.length != 2 || data[0][1] != 0)) {
           series.push({
             label: name,
             data: data,
@@ -116,14 +209,66 @@
         }
       }
       iterChilds(tree);
+      if(!plots[tree.channelId]) {
+        plots[tree.channelId] = $.plot(placeholder, series, {
+          xaxis: {
+            mode: "time",
+            timeBase: "milliseconds",
+            timezone: "browser",
+            min: viewMin ? viewMin : startDate.getTime(),
+            max: viewMax ? viewMax : endDate.getTime(),
+            zoomRange: [60000, null],
+            panRange: eventTimeRange
+          },
+          yaxis: {
+            zoomRange: false,
+            panRange: false
+          },
+          zoom: {
+            interactive: true
+          },
+          pan: {
+            interactive: true
+          },
+          legend: {
+            position: 'nw'
+          }
+        });
+        $(placeholder).bind("plotpan plotzoom", function (event, plot) {
+          var axes = plot.getAxes();
+          viewMin = axes.xaxis.min;
+          viewMax = axes.xaxis.max;
+          for(const id in plots) {
+            plots[id].getOptions().xaxes[0].min = axes.xaxis.min;
+            plots[id].getOptions().xaxes[0].max = axes.xaxis.max;
+            plots[id].setupGrid();
+            plots[id].draw();
+          }
+        });
+      } else {
+        plots[tree.channelId].getOptions().xaxis.panRange = eventTimeRange;
+        plots[tree.channelId].setData(series);
+        plots[tree.channelId].setupGrid();
+        plots[tree.channelId].draw();
+      }
+    }
 
-      return $.plot(div, series, {
-        xaxis: {
-          mode: "time",
-          timeBase: "milliseconds",
-          timezone: "browser"
-        }
-      });
+    function updatePlots() {
+      viewMin = startDate.getTime();
+      viewMax = endDate.getTime();
+      for(const id in plots) {
+        let opt = plots[id].getXAxes()[0].options;
+        opt.min = viewMin;
+        opt.max = viewMax;
+        plots[id].setupGrid();
+        plots[id].draw();
+      }
+    }
+
+    function addDays(date, days) {
+      var result = new Date(date);
+      result.setDate(result.getDate() + days);
+      return result;
     }
     </script>
 	</body>

+ 63 - 23
ts3-viewer/stylesheet.css

@@ -1,40 +1,80 @@
-.channel {
-  width: 300px;
-  overflow-x: hidden;
+body {
+
+}
+.wrapper {
+	display: grid;
+	grid-template-areas:
+		"header header"
+		"tree settings"
+		"tree graphs";
+	grid-template-columns: 400px auto;
+	grid-template-rows: auto 50px auto;
+	width: 100%;
+	grid-gap: 30px;
+}
+.main-head {
+	grid-area: header;
 }
 #tree {
-  float: left;
+	grid-area: tree;
+	clear: both;
+}
+#tree li {
+  width: 100%;
+}
+#tree li.spacer {
+	margin-top: 20px;
+}
+#tree li.spacer > label .channel {
+	font-weight: bold;
+}
+
+#tree label .channel {
+	width: 100%;
+}
+ul {
+	list-style-type: none;
+	padding-left: 10px;
+	border-top: dotted 1px grey;
+	border-left: solid 1px grey;
+}
+
+#tree .count {
+	float: right;
+}
+#tree .checkbox {
+	float: right;
+	clear: right;
+}
+#graphs {
+	grid-area: graphs;
+	display: flex;
+	flex-wrap: wrap;
+	align-content: flex-start;
+	align-items: stretch;
 }
 .demo-container {
 	box-sizing: border-box;
-	width: 850px;
-	height: 450px;
+	min-width: 850px;
+	min-height: 400px;
+	max-width: 100vw;
+	max-height: 100vh;
 	padding: 20px 15px 15px 15px;
 	margin: 15px auto 30px auto;
 	border: 1px solid #ddd;
 	background: #fff;
 	background: linear-gradient(#f6f6f6 0, #fff 50px);
-	background: -o-linear-gradient(#f6f6f6 0, #fff 50px);
-	background: -ms-linear-gradient(#f6f6f6 0, #fff 50px);
-	background: -moz-linear-gradient(#f6f6f6 0, #fff 50px);
-	background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px);
 	box-shadow: 0 3px 10px rgba(0,0,0,0.15);
-	-o-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-	-ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-	-moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-	-webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-	-webkit-tap-highlight-color: rgba(0,0,0,0);
-	-webkit-tap-highlight-color: transparent;
-	-webkit-touch-callout: none;
-	-webkit-user-select: none;
-	-khtml-user-select: none;
-	-moz-user-select: none;
-	-ms-user-select: none;
-  user-select: none;
-  float: left;
+
+	user-select: none;
+	flex-grow: 1;
 }
 .demo-placeholder {
 	width: 100%;
 	height: 100%;
 	font-size: 14px;
+}
+
+#settings {
+	grid-area: settings;
 }