mirror of
https://github.com/pi-hole/web.git
synced 2024-12-06 19:36:21 +01:00
Add long-term graph page (#542)
* Add api_db.php?getGraphData Signed-off-by: DL6ER <dl6er@dl6er.de> * Add new DB Graph page Signed-off-by: DL6ER <dl6er@dl6er.de> * Adjust x-axis format Signed-off-by: DL6ER <dl6er@dl6er.de> * Tweaks to long term stats (#543) * add maxDate to Datepicker * remove `GO` Button from datepicker input, instead hook into the apply event to update the graphs/charts when the date range is selected * :codacy: Signed-off-by: DL6ER <dl6er@dl6er.de> * Fix comment + another :codacy: * Remove duplicated code * Add click callback to DB graph that sends the user to the DB query log page. This required some re-write of the DB queries page. * Remove uneeded global variable * Remove minDate and MaxDate as it is causing issues on fresh installs * Oh :codacy: ...
This commit is contained in:
+47
@@ -159,6 +159,53 @@ if (isset($_GET['getDBfilesize']) && $auth)
|
||||
$data = array_merge($data, $result);
|
||||
}
|
||||
|
||||
if (isset($_GET['getGraphData']) && $auth)
|
||||
{
|
||||
$limit = "";
|
||||
|
||||
if(isset($_GET["from"]) && isset($_GET["until"]))
|
||||
{
|
||||
$limit = " AND timestamp >= ".intval($_GET["from"])." AND timestamp <= ".intval($_GET["until"]);
|
||||
}
|
||||
elseif(isset($_GET["from"]) && !isset($_GET["until"]))
|
||||
{
|
||||
$limit = " AND timestamp >= ".intval($_GET["from"]);
|
||||
}
|
||||
elseif(!isset($_GET["from"]) && isset($_GET["until"]))
|
||||
{
|
||||
$limit = " AND timestamp <= ".intval($_GET["until"]);
|
||||
}
|
||||
|
||||
$interval = 600;
|
||||
|
||||
if(isset($_GET["interval"]))
|
||||
{
|
||||
$q = intval($_GET["interval"]);
|
||||
if($q > 10)
|
||||
$interval = $q;
|
||||
}
|
||||
|
||||
// Count permitted queries in intervals
|
||||
$results = $db->query('SELECT (timestamp/'.$interval.')*'.$interval.' interval, COUNT(*) FROM queries WHERE (status == 2 OR status == 3)'.$limit.' GROUP by interval ORDER by interval');
|
||||
$domains = array();
|
||||
while ($row = $results->fetchArray())
|
||||
{
|
||||
$domains[$row[0]] = intval($row[1]);
|
||||
}
|
||||
$result = array('domains_over_time' => $domains);
|
||||
$data = array_merge($data, $result);
|
||||
|
||||
// Count blocked queries in intervals
|
||||
$results = $db->query('SELECT (timestamp/'.$interval.')*'.$interval.' interval, COUNT(*) FROM queries WHERE (status == 1 OR status == 4 OR status == 5)'.$limit.' GROUP by interval ORDER by interval');
|
||||
$addomains = array();
|
||||
while ($row = $results->fetchArray())
|
||||
{
|
||||
$addomains[$row[0]] = intval($row[1]);
|
||||
}
|
||||
$result = array('ads_over_time' => $addomains);
|
||||
$data = array_merge($data, $result);
|
||||
}
|
||||
|
||||
if(isset($_GET["jsonForceObject"]))
|
||||
{
|
||||
echo json_encode($data, JSON_FORCE_OBJECT);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php /*
|
||||
* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
require "scripts/pi-hole/php/header.php";
|
||||
|
||||
// Generate CSRF token
|
||||
if(empty($_SESSION['token'])) {
|
||||
$_SESSION['token'] = base64_encode(openssl_random_pseudo_bytes(32));
|
||||
}
|
||||
$token = $_SESSION['token'];
|
||||
|
||||
?>
|
||||
<!-- Send PHP info to JS -->
|
||||
<div id="token" hidden><?php echo $token ?></div>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="page-header">
|
||||
<h1>Compute graphical statistics from the Pi-hole query database</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<!-- Date Input -->
|
||||
<div class="form-group">
|
||||
<label>Date and time range:</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</div>
|
||||
<input type="text" class="form-control pull-right" id="querytime">
|
||||
</div>
|
||||
<!-- /.input group -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box" id="queries-over-time">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Queries over the selected time period</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="chart">
|
||||
<canvas id="queryOverTimeChart" width="800" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay" hidden="true">
|
||||
<i class="fa fa-refresh fa-spin"></i>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require "scripts/pi-hole/php/footer.php";
|
||||
?>
|
||||
|
||||
<script src="scripts/vendor/moment.min.js"></script>
|
||||
<script src="scripts/vendor/daterangepicker.js"></script>
|
||||
<script src="scripts/pi-hole/js/db_graph.js"></script>
|
||||
@@ -34,9 +34,6 @@ $token = $_SESSION['token'];
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</div>
|
||||
<input type="text" class="form-control pull-right" id="querytime">
|
||||
<span class="input-group-btn">
|
||||
<button id="btnGo" class="btn btn-default" type="button">Go!</button>
|
||||
</span>
|
||||
</div>
|
||||
<!-- /.input group -->
|
||||
</div>
|
||||
|
||||
@@ -34,9 +34,6 @@ $token = $_SESSION['token'];
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</div>
|
||||
<input type="text" class="form-control pull-right" id="querytime">
|
||||
<span class="input-group-btn">
|
||||
<button id="btnGo" class="btn btn-default" type="button">Go!</button>
|
||||
</span>
|
||||
</div>
|
||||
<!-- /.input group -->
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/*global
|
||||
moment
|
||||
*/
|
||||
|
||||
var start__ = moment().subtract(6, "days");
|
||||
var from = moment(start__).utc().valueOf()/1000;
|
||||
var end__ = moment();
|
||||
var until = moment(end__).utc().valueOf()/1000;
|
||||
|
||||
$(function () {
|
||||
$("#querytime").daterangepicker(
|
||||
{
|
||||
timePicker: true, timePickerIncrement: 15,
|
||||
locale: { format: "MMMM Do YYYY, HH:mm" },
|
||||
ranges: {
|
||||
"Today": [moment().startOf("day"), moment()],
|
||||
"Yesterday": [moment().subtract(1, "days").startOf("day"), moment().subtract(1, "days").endOf("day")],
|
||||
"Last 7 Days": [moment().subtract(6, "days"), moment()],
|
||||
"Last 30 Days": [moment().subtract(29, "days"), moment()],
|
||||
"This Month": [moment().startOf("month"), moment()],
|
||||
"Last Month": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")],
|
||||
"This Year": [moment().startOf("year"), moment()],
|
||||
"All Time": [moment(0), moment()]
|
||||
},
|
||||
"opens": "center", "showDropdowns": true
|
||||
},
|
||||
function (startt, endt) {
|
||||
from = moment(startt).utc().valueOf()/1000;
|
||||
until = moment(endt).utc().valueOf()/1000;
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Credit: http://stackoverflow.com/questions/1787322/htmlspecialchars-equivalent-in-javascript/4835406#4835406
|
||||
function escapeHtml(text) {
|
||||
var map = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"\"": """,
|
||||
"\'": "'"
|
||||
};
|
||||
|
||||
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||
}
|
||||
|
||||
function padNumber(num) {
|
||||
return ("00" + num).substr(-2,2);
|
||||
}
|
||||
|
||||
// Helper function needed for converting the Objects to Arrays
|
||||
|
||||
function objectToArray(p){
|
||||
var keys = Object.keys(p);
|
||||
keys.sort(function(a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
var arr = [], idx = [];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
arr.push(p[keys[i]]);
|
||||
idx.push(keys[i]);
|
||||
}
|
||||
return [idx,arr];
|
||||
}
|
||||
|
||||
var timeLineChart;
|
||||
|
||||
function compareNumbers(a, b) {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
function updateQueriesOverTime() {
|
||||
$("#queries-over-time .overlay").show();
|
||||
$.getJSON("api_db.php?getGraphData&from="+from+"&until="+until, function(data) {
|
||||
|
||||
// convert received objects to arrays
|
||||
data.domains_over_time = objectToArray(data.domains_over_time);
|
||||
data.ads_over_time = objectToArray(data.ads_over_time);
|
||||
// Remove possibly already existing data
|
||||
timeLineChart.data.labels = [];
|
||||
timeLineChart.data.datasets[0].data = [];
|
||||
timeLineChart.data.datasets[1].data = [];
|
||||
|
||||
var dates = [], hour;
|
||||
|
||||
for (hour in data.domains_over_time[0]) {
|
||||
if ({}.hasOwnProperty.call(data.domains_over_time[0], hour)) {
|
||||
dates.push(parseInt(data.domains_over_time[0][hour]));
|
||||
}
|
||||
}
|
||||
|
||||
for (hour in data.ads_over_time[0]) {
|
||||
if ({}.hasOwnProperty.call(data.ads_over_time[0], hour)) {
|
||||
if(!dates.includes(parseInt(data.ads_over_time[0][hour])))
|
||||
{
|
||||
dates.push(parseInt(data.ads_over_time[0][hour]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dates.sort(compareNumbers);
|
||||
|
||||
// Add data for each hour that is available
|
||||
for (hour in dates) {
|
||||
if ({}.hasOwnProperty.call(dates, hour)) {
|
||||
var d, dom = 0, ads = 0;
|
||||
d = new Date(1000*dates[hour]);
|
||||
|
||||
var idx = data.domains_over_time[0].indexOf(dates[hour].toString());
|
||||
if (idx > -1)
|
||||
{
|
||||
dom = data.domains_over_time[1][idx];
|
||||
}
|
||||
|
||||
idx = data.ads_over_time[0].indexOf(dates[hour].toString());
|
||||
if (idx > -1)
|
||||
{
|
||||
ads = data.ads_over_time[1][idx];
|
||||
}
|
||||
|
||||
timeLineChart.data.labels.push(d);
|
||||
timeLineChart.data.datasets[0].data.push(dom);
|
||||
timeLineChart.data.datasets[1].data.push(ads);
|
||||
}
|
||||
}
|
||||
|
||||
timeLineChart.options.scales.xAxes[0].display=true;
|
||||
$("#queries-over-time .overlay").hide();
|
||||
timeLineChart.update();
|
||||
});
|
||||
}
|
||||
|
||||
/* global Chart */
|
||||
$(document).ready(function() {
|
||||
var ctx = document.getElementById("queryOverTimeChart").getContext("2d");
|
||||
timeLineChart = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: [ 0 ],
|
||||
datasets: [
|
||||
{
|
||||
label: "Total DNS Queries",
|
||||
fill: true,
|
||||
backgroundColor: "rgba(220,220,220,0.5)",
|
||||
borderColor: "rgba(0, 166, 90,.8)",
|
||||
pointBorderColor: "rgba(0, 166, 90,.8)",
|
||||
pointRadius: 1,
|
||||
pointHoverRadius: 5,
|
||||
data: [],
|
||||
pointHitRadius: 5,
|
||||
cubicInterpolationMode: "monotone"
|
||||
},
|
||||
{
|
||||
label: "Blocked DNS Queries",
|
||||
fill: true,
|
||||
backgroundColor: "rgba(0,192,239,0.5)",
|
||||
borderColor: "rgba(0,192,239,1)",
|
||||
pointBorderColor: "rgba(0,192,239,1)",
|
||||
pointRadius: 1,
|
||||
pointHoverRadius: 5,
|
||||
data: [],
|
||||
pointHitRadius: 5,
|
||||
cubicInterpolationMode: "monotone"
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
tooltips: {
|
||||
enabled: true,
|
||||
responsive: true,
|
||||
mode: "x-axis",
|
||||
callbacks: {
|
||||
title: function(tooltipItem, data) {
|
||||
var label = tooltipItem[0].xLabel;
|
||||
var time = new Date(label);
|
||||
var date = time.getFullYear()+"-"+padNumber(time.getMonth())+"-"+padNumber(time.getDate());
|
||||
var h = time.getHours();
|
||||
var m = time.getMinutes();
|
||||
var from = padNumber(h)+":"+padNumber(m)+":00";
|
||||
var to = padNumber(h)+":"+padNumber(m+9)+":59";
|
||||
return "Queries from "+from+" to "+to+" on "+date;
|
||||
},
|
||||
label: function(tooltipItems, data) {
|
||||
if(tooltipItems.datasetIndex === 1)
|
||||
{
|
||||
var percentage = 0.0;
|
||||
var total = parseInt(data.datasets[0].data[tooltipItems.index]);
|
||||
var blocked = parseInt(data.datasets[1].data[tooltipItems.index]);
|
||||
if(total > 0)
|
||||
{
|
||||
percentage = 100.0*blocked/total;
|
||||
}
|
||||
return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel + " (" + percentage.toFixed(1) + "%)";
|
||||
}
|
||||
else
|
||||
{
|
||||
return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: "time",
|
||||
display: false,
|
||||
time: {
|
||||
displayFormats: {
|
||||
"minute": "HH:mm",
|
||||
"hour": "HH:mm",
|
||||
"day": "HH:mm",
|
||||
"week": "MMM DD HH:mm",
|
||||
"month": "MMM DD",
|
||||
"quarter": "MMM DD",
|
||||
"year": "MMM DD"
|
||||
}
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}]
|
||||
},
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#querytime").on("apply.daterangepicker", function(ev, picker) {
|
||||
$("#queries-over-time").show();
|
||||
updateQueriesOverTime();
|
||||
});
|
||||
|
||||
$("#queryOverTimeChart").click(function(evt){
|
||||
var activePoints = timeLineChart.getElementAtEvent(evt);
|
||||
if(activePoints.length > 0)
|
||||
{
|
||||
//get the internal index in the chart
|
||||
var clickedElementindex = activePoints[0]["_index"];
|
||||
|
||||
//get specific label by index
|
||||
var label = timeLineChart.data.labels[clickedElementindex];
|
||||
|
||||
//get value by index
|
||||
var from = label/1000;
|
||||
var until = label/1000 + 600;
|
||||
window.location.href = "db_queries.php?from="+from+"&until="+until;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -15,15 +15,6 @@ var end__ = moment();
|
||||
var until = moment(end__).utc().valueOf()/1000;
|
||||
|
||||
$(function () {
|
||||
// Get first time stamp we have valid data for to limit selectable date/time range
|
||||
$.getJSON("api_db.php?getMinTimestamp", function(data) {
|
||||
var minDate = parseInt(data.mintimestamp);
|
||||
if(!isNaN(minDate))
|
||||
{
|
||||
$("#querytime").data("daterangepicker").minDate = moment.unix(minDate);
|
||||
}
|
||||
});
|
||||
|
||||
$("#querytime").daterangepicker(
|
||||
{
|
||||
timePicker: true, timePickerIncrement: 15,
|
||||
@@ -38,7 +29,6 @@ $(function () {
|
||||
"This Year": [moment().startOf("year"), moment()],
|
||||
"All Time": [moment(0), moment()]
|
||||
},
|
||||
startDate: start__, endDate: end__,
|
||||
"opens": "center", "showDropdowns": true
|
||||
},
|
||||
function (startt, endt) {
|
||||
@@ -184,8 +174,7 @@ function updateTopAdsChart() {
|
||||
});
|
||||
}
|
||||
|
||||
// Handle "Go" Button
|
||||
$("#btnGo").on("click", function() {
|
||||
$("#querytime").on("apply.daterangepicker", function(ev, picker) {
|
||||
updateTopClientsChart();
|
||||
updateTopDomainsChart();
|
||||
updateTopAdsChart();
|
||||
|
||||
@@ -13,18 +13,24 @@ var start__ = moment().subtract(6, "days");
|
||||
var from = moment(start__).utc().valueOf()/1000;
|
||||
var end__ = moment();
|
||||
var until = moment(end__).utc().valueOf()/1000;
|
||||
var instantquery = false;
|
||||
var daterange;
|
||||
|
||||
// Do we want to filter queries?
|
||||
var GETDict = {};
|
||||
location.search.substr(1).split("&").forEach(function(item) {GETDict[item.split("=")[0]] = item.split("=")[1];});
|
||||
|
||||
if("from" in GETDict && "until" in GETDict)
|
||||
{
|
||||
from = parseInt(GETDict["from"]);
|
||||
until = parseInt(GETDict["until"]);
|
||||
start__ = moment(1000*from);
|
||||
end__ = moment(1000*until);
|
||||
instantquery = true;
|
||||
}
|
||||
|
||||
$(function () {
|
||||
// Get first time stamp we have valid data for to limit selectable date/time range
|
||||
$.getJSON("api_db.php?getMinTimestamp", function(data) {
|
||||
var minDate = parseInt(data.mintimestamp);
|
||||
if(!isNaN(minDate))
|
||||
{
|
||||
$("#querytime").data("daterangepicker").minDate = moment.unix(minDate);
|
||||
}
|
||||
});
|
||||
|
||||
$("#querytime").daterangepicker(
|
||||
daterange = $("#querytime").daterangepicker(
|
||||
{
|
||||
timePicker: true, timePickerIncrement: 15,
|
||||
locale: { format: "MMMM Do YYYY, HH:mm" },
|
||||
@@ -38,7 +44,6 @@ $(function () {
|
||||
"This Year": [moment().startOf("year"), moment()],
|
||||
"All Time": [moment(0), moment()]
|
||||
},
|
||||
startDate: start__, endDate: end__,
|
||||
"opens": "center", "showDropdowns": true
|
||||
},
|
||||
function (startt, endt) {
|
||||
@@ -174,6 +179,16 @@ function refreshTableData() {
|
||||
$(document).ready(function() {
|
||||
var status;
|
||||
|
||||
var APIstring;
|
||||
if(instantquery)
|
||||
{
|
||||
APIstring = "api_db.php?getAllQueries&from="+from+"&until="+until;
|
||||
}
|
||||
else
|
||||
{
|
||||
APIstring = "api_db.php?getAllQueries=empty";
|
||||
}
|
||||
|
||||
tableApi = $("#all-queries").DataTable( {
|
||||
"rowCallback": function( row, data, index ){
|
||||
if (data[4] === 1)
|
||||
@@ -215,7 +230,7 @@ $(document).ready(function() {
|
||||
"<'row'<'col-sm-4'l><'col-sm-8'p>>" +
|
||||
"<'row'<'col-sm-12'tr>>" +
|
||||
"<'row'<'col-sm-5'i><'col-sm-7'p>>",
|
||||
"ajax": {"url": "api_db.php?getAllQueries=empty", "error": handleAjaxError },
|
||||
"ajax": {"url": APIstring, "error": handleAjaxError },
|
||||
"autoWidth" : false,
|
||||
"processing": true,
|
||||
"deferRender": true,
|
||||
@@ -233,7 +248,8 @@ $(document).ready(function() {
|
||||
"targets": -1,
|
||||
"data": null,
|
||||
"defaultContent": ""
|
||||
} ]
|
||||
} ],
|
||||
"initComplete": reloadCallback
|
||||
});
|
||||
$("#all-queries tbody").on( "click", "button", function () {
|
||||
var data = tableApi.row( $(this).parents("tr") ).data();
|
||||
@@ -246,9 +262,14 @@ $(document).ready(function() {
|
||||
add(data[2],"black");
|
||||
}
|
||||
} );
|
||||
|
||||
if(instantquery)
|
||||
{
|
||||
daterange.val(start__.format("MMMM Do YYYY, HH:mm") + " - " + end__.format("MMMM Do YYYY, HH:mm"));
|
||||
}
|
||||
} );
|
||||
|
||||
// Handle "Go" Button
|
||||
$("#btnGo").on("click", function() {
|
||||
$("#querytime").on("apply.daterangepicker", function(ev, picker) {
|
||||
refreshTableData();
|
||||
});
|
||||
|
||||
|
||||
@@ -417,7 +417,7 @@ if($auth) {
|
||||
<i class="fa fa-file-text-o"></i> <span>Query Log</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="treeview<?php if($scriptname === "db_queries.php" || $scriptname === "db_lists.php"){ ?> active<?php } ?>">
|
||||
<li class="treeview<?php if($scriptname === "db_queries.php" || $scriptname === "db_lists.php" || $scriptname === "db_graph.php"){ ?> active<?php } ?>">
|
||||
<a href="#">
|
||||
<i class="fa fa-clock-o"></i> <span>Long term data</span>
|
||||
<span class="pull-right-container">
|
||||
@@ -425,6 +425,11 @@ if($auth) {
|
||||
</span>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
<li<?php if($scriptname === "db_graph.php"){ ?> class="active"<?php } ?>>
|
||||
<a href="db_graph.php">
|
||||
<i class="fa fa-file-text-o"></i> <span>Graphics</span>
|
||||
</a>
|
||||
</li>
|
||||
<li<?php if($scriptname === "db_queries.php"){ ?> class="active"<?php } ?>>
|
||||
<a href="db_queries.php">
|
||||
<i class="fa fa-file-text-o"></i> <span>Query Log</span>
|
||||
|
||||
Reference in New Issue
Block a user