mirror of
https://github.com/pi-hole/web.git
synced 2024-12-06 19:36:21 +01:00
Merge branch 'master' into master-to-development-v5.0
Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
+15
@@ -368,15 +368,30 @@ if (isset($_GET['getGraphData']) && $auth)
|
||||
// Parse the DB result into graph data, filling in missing interval sections with zero
|
||||
function parseDBData($results, $interval, $from, $until) {
|
||||
$data = array();
|
||||
$first_db_timestamp = -1;
|
||||
|
||||
if(!is_bool($results)) {
|
||||
// Read in the data
|
||||
while($row = $results->fetchArray()) {
|
||||
// $data[timestamp] = value_in_this_interval
|
||||
$data[$row[0]] = intval($row[1]);
|
||||
if($first_db_timestamp === -1)
|
||||
$first_db_timestamp = intval($row[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// It is unpredictable what the first timestamp returned by the database
|
||||
// will be. This depends on live data. Hence, we re-align the FROM
|
||||
// timestamp to avoid unaligned holes appearing as additional
|
||||
// (incorrect) data points
|
||||
$aligned_from = $from + (($first_db_timestamp - $from) % $interval);
|
||||
|
||||
// Fill gaps in returned data
|
||||
for($i = $aligned_from; $i < $until; $i += $interval) {
|
||||
if(!array_key_exists($i, $data))
|
||||
$data[$i] = 0;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php /*
|
||||
<?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.
|
||||
@@ -12,7 +12,7 @@
|
||||
<h1>Generate debug log</h1>
|
||||
</div>
|
||||
|
||||
<p><input type="checkbox" id="upload" checked> Upload debug log and provide token once finished</p>
|
||||
<p><input type="checkbox" id="upload"> Upload debug log and provide token once finished</p>
|
||||
<p>Once you click this button a debug log will be generated and can automatically be uploaded if we detect a working internet connection.</p>
|
||||
<button class="btn btn-lg btn-primary btn-block" id="debugBtn">Generate debug log</button>
|
||||
<pre id="output" style="width: 100%; height: 100%;" hidden="true"></pre>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
<!-- Title -->
|
||||
<div class="page-header">
|
||||
<h1>Custom DNS configuration</h1>
|
||||
<small>On this page, you can add domain/IP associations similar to records in <code>/etc/hosts</code>.</small>
|
||||
<h1>Local DNS Records</h1>
|
||||
<small>On this page, you can add domain/IP associations</small>
|
||||
</div>
|
||||
|
||||
<!-- Domain Input -->
|
||||
@@ -28,11 +28,11 @@
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="domain">Domain:</label>
|
||||
<input id="domain" type="url" class="form-control" placeholder="Add a domain (example.com or sub.example.com)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="ip">IP Address:</label>
|
||||
<input id="ip" type="text" class="form-control" placeholder="Associated IP address" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@
|
||||
<div class="box" id="recent-queries">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">
|
||||
List of custom DNS domains
|
||||
List of local DNS domains
|
||||
</h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
+5
-5
@@ -26,12 +26,12 @@
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="ex1">Address:</label>
|
||||
<input id="new_address" type="url" class="form-control" placeholder="http://..., https://..., file://..." autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="new_address">Address:</label>
|
||||
<input id="new_address" type="text" class="form-control" placeholder="http://..., https://..., file://..." autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="ex2">Comment:</label>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="new_comment">Comment:</label>
|
||||
<input id="new_comment" type="text" class="form-control" placeholder="Adlist description (optional)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+4
-4
@@ -26,15 +26,15 @@
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="ex1">Known clients:</label>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="select">Known clients:</label>
|
||||
<select id="select" class="form-control" placeholder="">
|
||||
<option disabled selected>Loading...</option>
|
||||
</select><br>
|
||||
<input id="ip-custom" type="text" class="form-control" disabled placeholder="Client IP address (IPv4 or IPv6, CIDR subnetting available, optional)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="ex3">Comment:</label>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="new_comment">Comment:</label>
|
||||
<input id="new_comment" type="text" class="form-control" placeholder="Client description (optional)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+4
-4
@@ -26,12 +26,12 @@
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="ex1">Name:</label>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="new_name">Name:</label>
|
||||
<input id="new_name" type="text" class="form-control" placeholder="Group name">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="ex2">Description:</label>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="new_desc">Description:</label>
|
||||
<input id="new_desc" type="text" class="form-control" placeholder="Group description (optional)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -140,23 +140,23 @@ function updateQueriesOverTime() {
|
||||
for (hour in dates) {
|
||||
if (Object.prototype.hasOwnProperty.call(dates, hour)) {
|
||||
var date,
|
||||
dom = 0,
|
||||
ads = 0;
|
||||
total = 0,
|
||||
blocked = 0;
|
||||
date = 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];
|
||||
total = 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];
|
||||
blocked = data.ads_over_time[1][idx];
|
||||
}
|
||||
|
||||
timeLineChart.data.labels.push(date);
|
||||
timeLineChart.data.datasets[0].data.push(dom - ads);
|
||||
timeLineChart.data.datasets[1].data.push(ads);
|
||||
timeLineChart.data.datasets[0].data.push(blocked);
|
||||
timeLineChart.data.datasets[1].data.push(total - blocked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,28 +170,30 @@ function updateQueriesOverTime() {
|
||||
|
||||
$(document).ready(function () {
|
||||
var ctx = document.getElementById("queryOverTimeChart").getContext("2d");
|
||||
var blockedColor = "#999";
|
||||
var permittedColor = "#00a65a";
|
||||
timeLineChart = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: "Permitted DNS Queries",
|
||||
label: "Blocked DNS Queries",
|
||||
fill: true,
|
||||
backgroundColor: "rgba(0, 166, 90,.8)",
|
||||
borderColor: "rgba(0, 166, 90,.8)",
|
||||
pointBorderColor: "rgba(0, 166, 90,.8)",
|
||||
backgroundColor: blockedColor,
|
||||
borderColor: blockedColor,
|
||||
pointBorderColor: blockedColor,
|
||||
pointRadius: 1,
|
||||
pointHoverRadius: 5,
|
||||
data: [],
|
||||
pointHitRadius: 5
|
||||
},
|
||||
{
|
||||
label: "Blocked DNS Queries",
|
||||
label: "Permitted DNS Queries",
|
||||
fill: true,
|
||||
backgroundColor: "rgba(0,192,239,1)",
|
||||
borderColor: "rgba(0,192,239,1)",
|
||||
pointBorderColor: "rgba(0,192,239,1)",
|
||||
backgroundColor: permittedColor,
|
||||
borderColor: permittedColor,
|
||||
pointBorderColor: permittedColor,
|
||||
pointRadius: 1,
|
||||
pointHoverRadius: 5,
|
||||
data: [],
|
||||
@@ -202,6 +204,9 @@ $(document).ready(function () {
|
||||
options: {
|
||||
tooltips: {
|
||||
enabled: true,
|
||||
itemSort: function(a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
mode: "x-axis",
|
||||
callbacks: {
|
||||
title: function (tooltipItem) {
|
||||
@@ -212,8 +217,8 @@ $(document).ready(function () {
|
||||
"-" +
|
||||
padNumber(time.getMonth() + 1) +
|
||||
"-" +
|
||||
padNumber(time.getDate()) +
|
||||
" " +
|
||||
padNumber(time.getDate());
|
||||
var from_time =
|
||||
padNumber(time.getHours()) +
|
||||
":" +
|
||||
padNumber(time.getMinutes()) +
|
||||
@@ -225,22 +230,50 @@ $(document).ready(function () {
|
||||
"-" +
|
||||
padNumber(time.getMonth() + 1) +
|
||||
"-" +
|
||||
padNumber(time.getDate()) +
|
||||
" " +
|
||||
padNumber(time.getDate());
|
||||
var until_time =
|
||||
padNumber(time.getHours()) +
|
||||
":" +
|
||||
padNumber(time.getMinutes()) +
|
||||
":" +
|
||||
padNumber(time.getSeconds());
|
||||
return "Queries from " + from_date + " to " + until_date;
|
||||
|
||||
if (from_date === until_date) {
|
||||
// Abbreviated form for intervals on the same day
|
||||
// We split title in two lines on small screens
|
||||
if ($(window).width() < 992) {
|
||||
until_time += "\n";
|
||||
}
|
||||
|
||||
return ("Queries from " + from_time + " to " + until_time + " on " + from_date).split(
|
||||
"\n "
|
||||
);
|
||||
}
|
||||
|
||||
// Full tooltip for intervals spanning more than one day
|
||||
// We split title in two lines on small screens
|
||||
if ($(window).width() < 992) {
|
||||
from_date += "\n";
|
||||
}
|
||||
|
||||
return (
|
||||
"Queries from " +
|
||||
from_date +
|
||||
" " +
|
||||
from_time +
|
||||
" to " +
|
||||
until_date +
|
||||
" " +
|
||||
until_time
|
||||
).split("\n ");
|
||||
},
|
||||
label: function (tooltipItems, data) {
|
||||
if (tooltipItems.datasetIndex === 1) {
|
||||
label: function(tooltipItems, data) {
|
||||
if (tooltipItems.datasetIndex === 0) {
|
||||
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;
|
||||
var permitted = parseInt(data.datasets[1].data[tooltipItems.index]);
|
||||
var blocked = parseInt(data.datasets[0].data[tooltipItems.index]);
|
||||
if (permitted + blocked > 0) {
|
||||
percentage = (100.0 * blocked) / (permitted + blocked);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -322,6 +322,14 @@ $(document).ready(function () {
|
||||
$(row).css("color", color);
|
||||
$("td:eq(4)", row).html(fieldtext);
|
||||
$("td:eq(5)", row).html(buttontext);
|
||||
|
||||
// Substitute domain by "." if empty
|
||||
var domain = data[2];
|
||||
if (domain.length === 0) {
|
||||
domain = ".";
|
||||
}
|
||||
|
||||
$("td:eq(2)", row).text(domain);
|
||||
},
|
||||
dom:
|
||||
"<'row'<'col-sm-12'f>>" +
|
||||
|
||||
@@ -26,14 +26,8 @@ function get_groups() {
|
||||
$(document).ready(function () {
|
||||
$("#btnAdd").on("click", addAdlist);
|
||||
|
||||
utils.bsSelect_defaults();
|
||||
get_groups();
|
||||
|
||||
// Disable autocorrect in the search box
|
||||
var input = document.querySelector("input[type=search]");
|
||||
input.setAttribute("autocomplete", "off");
|
||||
input.setAttribute("autocorrect", "off");
|
||||
input.setAttribute("autocapitalize", "off");
|
||||
input.setAttribute("spellcheck", false);
|
||||
});
|
||||
|
||||
function initTable() {
|
||||
@@ -54,6 +48,8 @@ function initTable() {
|
||||
],
|
||||
drawCallback: function () {
|
||||
$('button[id^="deleteAdlist_"]').on("click", deleteAdlist);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
$(row).attr("data-id", data.id);
|
||||
@@ -95,61 +91,73 @@ function initTable() {
|
||||
|
||||
$("td:eq(3)", row).empty();
|
||||
$("td:eq(3)", row).append(
|
||||
'<div id="selectHome_' +
|
||||
data.id +
|
||||
'">' +
|
||||
'<select id="multiselect_' +
|
||||
data.id +
|
||||
'" multiple="multiple"></select></div>'
|
||||
'<select class="selectpicker" id="multiselect_' + data.id + '" multiple></select>'
|
||||
);
|
||||
var selectEl = $("#multiselect_" + data.id, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var extra = "";
|
||||
var data_sub = "";
|
||||
if (!groups[i].enabled) {
|
||||
extra = " (disabled)";
|
||||
data_sub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
|
||||
selectEl.append(
|
||||
$("<option />")
|
||||
$("<option " + data_sub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name + extra)
|
||||
.text(groups[i].name)
|
||||
);
|
||||
}
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// Initialize multiselect
|
||||
selectEl.multiselect({
|
||||
includeSelectAllOption: true,
|
||||
buttonContainer: '<div id="container_' + data.id + '" class="btn-group"/>',
|
||||
maxHeight: 200,
|
||||
onDropdownShown: function () {
|
||||
var el = $("#container_" + data.id);
|
||||
var top = el[0].getBoundingClientRect().top;
|
||||
var bottom = $(window).height() - top - el.height();
|
||||
if (bottom < 200) {
|
||||
el.addClass("dropup");
|
||||
// Initialize bootstrap-select
|
||||
selectEl
|
||||
// fix dropdown if it would stick out right of the viewport
|
||||
.on("show.bs.select", function() {
|
||||
var winWidth = $(window).width();
|
||||
var dropdownEl = $("body > .bootstrap-select.dropdown");
|
||||
if (dropdownEl.length > 0) {
|
||||
dropdownEl.removeClass("align-right");
|
||||
var width = dropdownEl.width();
|
||||
var left = dropdownEl.offset().left;
|
||||
if (left + width > winWidth) {
|
||||
dropdownEl.addClass("align-right");
|
||||
}
|
||||
}
|
||||
|
||||
if (bottom > 200) {
|
||||
el.removeClass("dropup");
|
||||
})
|
||||
.on("changed.bs.select", function() {
|
||||
// enable Apply button
|
||||
if ($(ApplyBtn).prop("disabled")) {
|
||||
$(ApplyBtn)
|
||||
.addClass("btn-success")
|
||||
.prop("disabled", false)
|
||||
.on("click", function() {
|
||||
editAdlist.call(selectEl);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on("hide.bs.select", function() {
|
||||
// Restore values if drop-down menu is closed without clicking the Apply button
|
||||
if (!$(ApplyBtn).prop("disabled")) {
|
||||
$(this)
|
||||
.val(data.groups)
|
||||
.selectpicker("refresh");
|
||||
$(ApplyBtn)
|
||||
.removeClass("btn-success")
|
||||
.prop("disabled", true)
|
||||
.off("click");
|
||||
}
|
||||
})
|
||||
.selectpicker()
|
||||
.siblings(".dropdown-menu")
|
||||
.find(".bs-actionsbox")
|
||||
.prepend(
|
||||
'<button type="button" id=btn_apply_' +
|
||||
data.id +
|
||||
' class="btn btn-block btn-sm" disabled>Apply</button>'
|
||||
);
|
||||
|
||||
var offset = el.offset();
|
||||
$("body").append(el);
|
||||
el.css("position", "absolute");
|
||||
el.css("top", offset.top + "px");
|
||||
el.css("left", offset.left + "px");
|
||||
},
|
||||
onDropdownHide: function () {
|
||||
var el = $("#container_" + data.id);
|
||||
var home = $("#selectHome_" + data.id);
|
||||
home.append(el);
|
||||
el.removeAttr("style");
|
||||
}
|
||||
});
|
||||
selectEl.on("change", editAdlist);
|
||||
var ApplyBtn = "#btn_apply_" + data.id;
|
||||
|
||||
var button =
|
||||
'<button class="btn btn-danger btn-xs" type="button" id="deleteAdlist_' +
|
||||
@@ -204,6 +212,13 @@ function initTable() {
|
||||
table.order([[0, "asc"]]).draw();
|
||||
$("#resetButton").hide();
|
||||
});
|
||||
|
||||
// Disable autocorrect in the search box
|
||||
var input = document.querySelector("input[type=search]");
|
||||
input.setAttribute("autocomplete", "off");
|
||||
input.setAttribute("autocorrect", "off");
|
||||
input.setAttribute("autocapitalize", "off");
|
||||
input.setAttribute("spellcheck", false);
|
||||
}
|
||||
|
||||
function addAdlist() {
|
||||
@@ -231,7 +246,13 @@ function addAdlist() {
|
||||
success: function (response) {
|
||||
utils.enableAll();
|
||||
if (response.success) {
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added adlist", address);
|
||||
utils.showAlert(
|
||||
"success",
|
||||
"glyphicon glyphicon-plus",
|
||||
"Successfully added adlist",
|
||||
address
|
||||
);
|
||||
table.ajax.reload(null, false);
|
||||
$("#new_address").val("");
|
||||
$("#new_comment").val("");
|
||||
table.ajax.reload();
|
||||
@@ -306,6 +327,7 @@ function editAdlist() {
|
||||
"Successfully " + done + " adlist ",
|
||||
address
|
||||
);
|
||||
table.ajax.reload(null, false);
|
||||
} else {
|
||||
utils.showAlert(
|
||||
"error",
|
||||
@@ -343,8 +365,17 @@ function deleteAdlist() {
|
||||
success: function (response) {
|
||||
utils.enableAll();
|
||||
if (response.success) {
|
||||
utils.showAlert("success", "far fa-trash-alt", "Successfully deleted adlist ", address);
|
||||
table.row(tr).remove().draw(false);
|
||||
utils.showAlert(
|
||||
"success",
|
||||
"glyphicon glyphicon-trash",
|
||||
"Successfully deleted adlist ",
|
||||
address
|
||||
);
|
||||
table
|
||||
.row(tr)
|
||||
.remove()
|
||||
.draw(false)
|
||||
.ajax.reload(null, false);
|
||||
} else {
|
||||
utils.showAlert("error", "", "Error while deleting adlist with ID " + id, response.message);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ $(document).ready(function () {
|
||||
$("#btnAdd").on("click", addClient);
|
||||
|
||||
reload_client_suggestions();
|
||||
utils.bsSelect_defaults();
|
||||
get_groups();
|
||||
|
||||
$("#select").on("change", function () {
|
||||
@@ -88,6 +89,8 @@ function initTable() {
|
||||
],
|
||||
drawCallback: function () {
|
||||
$('button[id^="deleteClient_"]').on("click", deleteClient);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
$(row).attr("data-id", data.id);
|
||||
@@ -124,61 +127,73 @@ function initTable() {
|
||||
|
||||
$("td:eq(2)", row).empty();
|
||||
$("td:eq(2)", row).append(
|
||||
'<div id="selectHome_' +
|
||||
data.id +
|
||||
'">' +
|
||||
'<select id="multiselect_' +
|
||||
data.id +
|
||||
'" multiple="multiple"></select></div>'
|
||||
'<select class="selectpicker" id="multiselect_' + data.id + '" multiple></select>'
|
||||
);
|
||||
var selectEl = $("#multiselect_" + data.id, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var extra = "";
|
||||
var data_sub = "";
|
||||
if (!groups[i].enabled) {
|
||||
extra = " (disabled)";
|
||||
data_sub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
|
||||
selectEl.append(
|
||||
$("<option />")
|
||||
$("<option " + data_sub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name + extra)
|
||||
.text(groups[i].name)
|
||||
);
|
||||
}
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// Initialize multiselect
|
||||
selectEl.multiselect({
|
||||
includeSelectAllOption: true,
|
||||
buttonContainer: '<div id="container_' + data.id + '" class="btn-group"/>',
|
||||
maxHeight: 200,
|
||||
onDropdownShown: function () {
|
||||
var el = $("#container_" + data.id);
|
||||
var top = el[0].getBoundingClientRect().top;
|
||||
var bottom = $(window).height() - top - el.height();
|
||||
if (bottom < 200) {
|
||||
el.addClass("dropup");
|
||||
// Initialize bootstrap-select
|
||||
selectEl
|
||||
// fix dropdown if it would stick out right of the viewport
|
||||
.on("show.bs.select", function() {
|
||||
var winWidth = $(window).width();
|
||||
var dropdownEl = $("body > .bootstrap-select.dropdown");
|
||||
if (dropdownEl.length > 0) {
|
||||
dropdownEl.removeClass("align-right");
|
||||
var width = dropdownEl.width();
|
||||
var left = dropdownEl.offset().left;
|
||||
if (left + width > winWidth) {
|
||||
dropdownEl.addClass("align-right");
|
||||
}
|
||||
}
|
||||
|
||||
if (bottom > 200) {
|
||||
el.removeClass("dropup");
|
||||
})
|
||||
.on("changed.bs.select", function() {
|
||||
// enable Apply button
|
||||
if ($(ApplyBtn).prop("disabled")) {
|
||||
$(ApplyBtn)
|
||||
.addClass("btn-success")
|
||||
.prop("disabled", false)
|
||||
.on("click", function() {
|
||||
editClient.call(selectEl);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on("hide.bs.select", function() {
|
||||
// Restore values if drop-down menu is closed without clicking the Apply button
|
||||
if (!$(ApplyBtn).prop("disabled")) {
|
||||
$(this)
|
||||
.val(data.groups)
|
||||
.selectpicker("refresh");
|
||||
$(ApplyBtn)
|
||||
.removeClass("btn-success")
|
||||
.prop("disabled", true)
|
||||
.off("click");
|
||||
}
|
||||
})
|
||||
.selectpicker()
|
||||
.siblings(".dropdown-menu")
|
||||
.find(".bs-actionsbox")
|
||||
.prepend(
|
||||
'<button type="button" id=btn_apply_' +
|
||||
data.id +
|
||||
' class="btn btn-block btn-sm" disabled>Apply</button>'
|
||||
);
|
||||
|
||||
var offset = el.offset();
|
||||
$("body").append(el);
|
||||
el.css("position", "absolute");
|
||||
el.css("top", offset.top + "px");
|
||||
el.css("left", offset.left + "px");
|
||||
},
|
||||
onDropdownHide: function () {
|
||||
var el = $("#container" + data.id);
|
||||
var home = $("#selectHome" + data.id);
|
||||
home.append(el);
|
||||
el.removeAttr("style");
|
||||
}
|
||||
});
|
||||
selectEl.on("change", editClient);
|
||||
var ApplyBtn = "#btn_apply_" + data.id;
|
||||
|
||||
var button =
|
||||
'<button class="btn btn-danger btn-xs" type="button" id="deleteClient_' +
|
||||
@@ -276,7 +291,7 @@ function addClient() {
|
||||
if (response.success) {
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added client", ip);
|
||||
reload_client_suggestions();
|
||||
table.ajax.reload();
|
||||
table.ajax.reload(null, false);
|
||||
} else {
|
||||
utils.showAlert("error", "", "Error while adding new client", response.message);
|
||||
}
|
||||
@@ -335,7 +350,13 @@ function editClient() {
|
||||
success: function (response) {
|
||||
utils.enableAll();
|
||||
if (response.success) {
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully " + done + " client", ip_name);
|
||||
utils.showAlert(
|
||||
"success",
|
||||
"glyphicon glyphicon-pencil",
|
||||
"Successfully " + done + " client",
|
||||
ip_name
|
||||
);
|
||||
table.ajax.reload(null, false);
|
||||
} else {
|
||||
utils.showAlert(
|
||||
"error",
|
||||
@@ -378,8 +399,17 @@ function deleteClient() {
|
||||
success: function (response) {
|
||||
utils.enableAll();
|
||||
if (response.success) {
|
||||
utils.showAlert("success", "far fa-trash-alt", "Successfully deleted client ", ip_name);
|
||||
table.row(tr).remove().draw(false);
|
||||
utils.showAlert(
|
||||
"success",
|
||||
"glyphicon glyphicon-trash",
|
||||
"Successfully deleted client ",
|
||||
ip_name
|
||||
);
|
||||
table
|
||||
.row(tr)
|
||||
.remove()
|
||||
.draw(false)
|
||||
.ajax.reload(null, false);
|
||||
reload_client_suggestions();
|
||||
} else {
|
||||
utils.showAlert("error", "", "Error while deleting client with ID " + id, response.message);
|
||||
|
||||
@@ -116,7 +116,7 @@ function validateIPv6CIDR(ip) {
|
||||
ipv6elem +
|
||||
"))*::((?:" +
|
||||
ipv6elem +
|
||||
"))((?::" +
|
||||
"))*((?::" +
|
||||
ipv6elem +
|
||||
"))*|((?:" +
|
||||
ipv6elem +
|
||||
@@ -129,13 +129,34 @@ function validateIPv6CIDR(ip) {
|
||||
return ipv6validator.test(ip);
|
||||
}
|
||||
|
||||
window.utils = (function () {
|
||||
function bsSelect_defaults() {
|
||||
// set bootstrap-select defaults
|
||||
var pickerDEFAULTS = $.fn.selectpicker.Constructor.DEFAULTS;
|
||||
pickerDEFAULTS.noneSelectedText = "none selected";
|
||||
pickerDEFAULTS.selectedTextFormat = "count > 1";
|
||||
pickerDEFAULTS.actionsBox = true;
|
||||
pickerDEFAULTS.width = "fit";
|
||||
pickerDEFAULTS.container = "body";
|
||||
pickerDEFAULTS.dropdownAlignRight = "auto";
|
||||
pickerDEFAULTS.selectAllText = "All";
|
||||
pickerDEFAULTS.deselectAllText = "None";
|
||||
pickerDEFAULTS.countSelectedText = function(num, total) {
|
||||
if (num === total) {
|
||||
return "All selected (" + num + ")";
|
||||
}
|
||||
|
||||
return num + " selected";
|
||||
};
|
||||
}
|
||||
|
||||
window.utils = (function() {
|
||||
return {
|
||||
showAlert: showAlert,
|
||||
datetime: datetime,
|
||||
disableAll: disableAll,
|
||||
enableAll: enableAll,
|
||||
validateIPv4CIDR: validateIPv4CIDR,
|
||||
validateIPv6CIDR: validateIPv6CIDR
|
||||
validateIPv6CIDR: validateIPv6CIDR,
|
||||
bsSelect_defaults: bsSelect_defaults
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -61,6 +61,7 @@ $(document).ready(function () {
|
||||
|
||||
$("#add2black, #add2white").on("click", addDomain);
|
||||
|
||||
utils.bsSelect_defaults();
|
||||
get_groups();
|
||||
});
|
||||
|
||||
@@ -83,6 +84,8 @@ function initTable() {
|
||||
],
|
||||
drawCallback: function () {
|
||||
$('button[id^="deleteDomain_"]').on("click", deleteDomain);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
$(row).attr("data-id", data.id);
|
||||
@@ -159,63 +162,75 @@ function initTable() {
|
||||
if (table.column(5).visible()) {
|
||||
$("td:eq(4)", row).empty();
|
||||
$("td:eq(4)", row).append(
|
||||
'<div id="selectHome_' +
|
||||
data.id +
|
||||
'">' +
|
||||
'<select id="multiselect_' +
|
||||
data.id +
|
||||
'" multiple="multiple"></select></div>'
|
||||
'<select class="selectpicker" id="multiselect_' + data.id + '" multiple></select>'
|
||||
);
|
||||
var selectEl = $("#multiselect_" + data.id, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var extra = "";
|
||||
var data_sub = "";
|
||||
if (!groups[i].enabled) {
|
||||
extra = " (disabled)";
|
||||
data_sub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
|
||||
selectEl.append(
|
||||
$("<option />")
|
||||
$("<option " + data_sub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name + extra)
|
||||
.text(groups[i].name)
|
||||
);
|
||||
}
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// Initialize multiselect
|
||||
selectEl.multiselect({
|
||||
includeSelectAllOption: true,
|
||||
buttonContainer: '<div id="container_' + data.id + '" class="btn-group"/>',
|
||||
maxHeight: 200,
|
||||
onDropdownShown: function () {
|
||||
var el = $("#container_" + data.id);
|
||||
var top = el[0].getBoundingClientRect().top;
|
||||
var bottom = $(window).height() - top - el.height();
|
||||
if (bottom < 200) {
|
||||
el.addClass("dropup");
|
||||
// Initialize bootstrap-select
|
||||
selectEl
|
||||
// fix dropdown if it would stick out right of the viewport
|
||||
.on("show.bs.select", function() {
|
||||
var winWidth = $(window).width();
|
||||
var dropdownEl = $("body > .bootstrap-select.dropdown");
|
||||
if (dropdownEl.length > 0) {
|
||||
dropdownEl.removeClass("align-right");
|
||||
var width = dropdownEl.width();
|
||||
var left = dropdownEl.offset().left;
|
||||
if (left + width > winWidth) {
|
||||
dropdownEl.addClass("align-right");
|
||||
}
|
||||
}
|
||||
|
||||
if (bottom > 200) {
|
||||
el.removeClass("dropup");
|
||||
})
|
||||
.on("changed.bs.select", function() {
|
||||
// enable Apply button
|
||||
if ($(ApplyBtn).prop("disabled")) {
|
||||
$(ApplyBtn)
|
||||
.addClass("btn-success")
|
||||
.prop("disabled", false)
|
||||
.on("click", function() {
|
||||
editDomain.call(selectEl);
|
||||
});
|
||||
}
|
||||
|
||||
var offset = el.offset();
|
||||
$("body").append(el);
|
||||
el.css("position", "absolute");
|
||||
el.css("top", offset.top + "px");
|
||||
el.css("left", offset.left + "px");
|
||||
},
|
||||
onDropdownHide: function () {
|
||||
var el = $("#container_" + data.id);
|
||||
var home = $("#selectHome_" + data.id);
|
||||
home.append(el);
|
||||
el.removeAttr("style");
|
||||
}
|
||||
});
|
||||
selectEl.on("change", editDomain);
|
||||
})
|
||||
.on("hide.bs.select", function() {
|
||||
// Restore values if drop-down menu is closed without clicking the Apply button
|
||||
if (!$(ApplyBtn).prop("disabled")) {
|
||||
$(this)
|
||||
.val(data.groups)
|
||||
.selectpicker("refresh");
|
||||
$(ApplyBtn)
|
||||
.removeClass("btn-success")
|
||||
.prop("disabled", true)
|
||||
.off("click");
|
||||
}
|
||||
})
|
||||
.selectpicker()
|
||||
.siblings(".dropdown-menu")
|
||||
.find(".bs-actionsbox")
|
||||
.prepend(
|
||||
'<button type="button" id=btn_apply_' +
|
||||
data.id +
|
||||
' class="btn btn-block btn-sm" disabled>Apply</button>'
|
||||
);
|
||||
}
|
||||
|
||||
var ApplyBtn = "#btn_apply_" + data.id;
|
||||
|
||||
// Highlight row (if url parameter "domainid=" is used)
|
||||
if ("domainid" in GETDict && data.id === parseInt(GETDict.domainid)) {
|
||||
$(row).find("td").addClass("highlight");
|
||||
@@ -360,7 +375,7 @@ function addDomain() {
|
||||
domainEl.val("");
|
||||
commentEl.val("");
|
||||
wildcardEl.prop("checked", false);
|
||||
table.ajax.reload();
|
||||
table.ajax.reload(null, false);
|
||||
} else {
|
||||
utils.showAlert("error", "", "Error while adding new " + domain_regex, response.message);
|
||||
}
|
||||
@@ -452,6 +467,7 @@ function editDomain() {
|
||||
"Successfully " + done + " " + domain_regex,
|
||||
domain
|
||||
);
|
||||
table.ajax.reload(null, false);
|
||||
} else
|
||||
utils.showAlert(
|
||||
"error",
|
||||
@@ -502,7 +518,11 @@ function deleteDomain() {
|
||||
"Successfully deleted " + domain_regex,
|
||||
domain
|
||||
);
|
||||
table.row(tr).remove().draw(false);
|
||||
table
|
||||
.row(tr)
|
||||
.remove()
|
||||
.draw(false)
|
||||
.ajax.reload(null, false);
|
||||
} else {
|
||||
utils.showAlert(
|
||||
"error",
|
||||
|
||||
+178
-71
@@ -32,16 +32,26 @@ function objectToArray(p) {
|
||||
return [idx, arr];
|
||||
}
|
||||
|
||||
var lastTooltipTime = 0;
|
||||
|
||||
var customTooltips = function (tooltip) {
|
||||
// Tooltip Element
|
||||
var tooltipEl = document.getElementById("chartjs-tooltip");
|
||||
var customTooltips = function(tooltip) {
|
||||
var tooltipEl = document.getElementById(this._chart.canvas.id + "-customTooltip");
|
||||
if (!tooltipEl) {
|
||||
// Create Tooltip Element once per chart
|
||||
tooltipEl = document.createElement("div");
|
||||
tooltipEl.id = "chartjs-tooltip";
|
||||
document.body.appendChild(tooltipEl);
|
||||
$(tooltipEl).html("<table></table>");
|
||||
tooltipEl.id = this._chart.canvas.id + "-customTooltip";
|
||||
tooltipEl.classList.add("chartjs-tooltip");
|
||||
tooltipEl.innerHTML = "<div class='arrow'></div> <table></table>";
|
||||
// avoid browser's font-zoom since we know that <body>'s
|
||||
// font-size was set to 14px by bootstrap's css
|
||||
var fontZoom = parseFloat($("body").css("font-size")) / 14;
|
||||
// set styles and font
|
||||
tooltipEl.style.padding = tooltip.yPadding + "px " + tooltip.xPadding + "px";
|
||||
tooltipEl.style.borderRadius = tooltip.cornerRadius + "px";
|
||||
tooltipEl.style.fontFamily = tooltip._bodyFontFamily;
|
||||
tooltipEl.style.fontSize = tooltip.bodyFontSize / fontZoom + "px";
|
||||
tooltipEl.style.fontStyle = tooltip._bodyFontStyle;
|
||||
// append Tooltip next to canvas-containing box
|
||||
tooltipEl.ancestor = this._chart.canvas.closest(".box[id]").parentNode;
|
||||
tooltipEl.ancestor.appendChild(tooltipEl);
|
||||
}
|
||||
|
||||
// Hide if no tooltip
|
||||
@@ -50,22 +60,9 @@ var customTooltips = function (tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit rendering to once every 50ms. This gives the DOM time to react,
|
||||
// and avoids "lag" caused by not giving the DOM time to reapply CSS.
|
||||
var now = Date.now();
|
||||
if (now - lastTooltipTime < 50) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastTooltipTime = now;
|
||||
|
||||
// Set caret Position
|
||||
tooltipEl.classList.remove("above", "below", "no-transform");
|
||||
if (tooltip.yAlign) {
|
||||
tooltipEl.classList.add(tooltip.yAlign);
|
||||
} else {
|
||||
tooltipEl.classList.add("above");
|
||||
}
|
||||
// Set caret position
|
||||
tooltipEl.classList.remove("left", "right", "center", "top", "bottom");
|
||||
tooltipEl.classList.add(tooltip.xAlign, tooltip.yAlign);
|
||||
|
||||
function getBody(bodyItem) {
|
||||
return bodyItem.lines;
|
||||
@@ -75,22 +72,26 @@ var customTooltips = function (tooltip) {
|
||||
if (tooltip.body) {
|
||||
var titleLines = tooltip.title || [];
|
||||
var bodyLines = tooltip.body.map(getBody);
|
||||
var innerHtml = "<table><thead>";
|
||||
titleLines.forEach(function (title) {
|
||||
var innerHtml = "<thead>";
|
||||
|
||||
titleLines.forEach(function(title) {
|
||||
innerHtml += "<tr><th>" + title + "</th></tr>";
|
||||
});
|
||||
innerHtml += "</thead><tbody>";
|
||||
var printed = 0;
|
||||
bodyLines.forEach(function (body, i) {
|
||||
|
||||
var devicePixel = (1 / window.devicePixelRatio).toFixed(1);
|
||||
bodyLines.forEach(function(body, i) {
|
||||
var colors = tooltip.labelColors[i];
|
||||
var style = "background:" + colors.backgroundColor;
|
||||
style += "; border-color:" + colors.borderColor;
|
||||
style += "; border-width: 2px";
|
||||
var span = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
|
||||
var style = "background: " + colors.backgroundColor;
|
||||
style += "; outline: 1px solid " + colors.backgroundColor;
|
||||
style += "; border: " + devicePixel + "px solid #fff";
|
||||
var span = "<span class='chartjs-tooltip-key' style='" + style + "'></span>";
|
||||
|
||||
var num = body[0].split(": ");
|
||||
// remove percent symbol from amount to allow numeric comparison
|
||||
var number = num[1].replace(/%/i, "");
|
||||
if (number > 0) {
|
||||
// do not display entries with value of 0 (in bar chart),
|
||||
// but pass through entries with "0.0% (in pie charts)
|
||||
if (num[1] !== "0") {
|
||||
innerHtml += "<tr><td>" + span + body + "</td></tr>";
|
||||
printed++;
|
||||
}
|
||||
@@ -99,30 +100,119 @@ var customTooltips = function (tooltip) {
|
||||
innerHtml += "<tr><td>No activity recorded</td></tr>";
|
||||
}
|
||||
|
||||
innerHtml += "</tbody></table>";
|
||||
$(tooltipEl).html(innerHtml);
|
||||
innerHtml += "</tbody>";
|
||||
|
||||
var tableRoot = tooltipEl.querySelector("table");
|
||||
tableRoot.innerHTML = innerHtml;
|
||||
}
|
||||
|
||||
// Display, position, and set styles for font
|
||||
var position = this._chart.canvas.getBoundingClientRect();
|
||||
var width = tooltip.caretX;
|
||||
// Prevent compression of the tooltip at the right edge of the screen
|
||||
if ($(document).width() - tooltip.caretX < 400) {
|
||||
width = $(document).width() - 400;
|
||||
var canvasPos = this._chart.canvas.getBoundingClientRect();
|
||||
var boxPos = tooltipEl.ancestor.getBoundingClientRect();
|
||||
var offsetX = canvasPos.left - boxPos.left;
|
||||
var offsetY = canvasPos.top - boxPos.top;
|
||||
var tooltipWidth = tooltipEl.offsetWidth;
|
||||
var tooltipHeight = tooltipEl.offsetHeight;
|
||||
var caretX = tooltip.caretX;
|
||||
var caretY = tooltip.caretY;
|
||||
var caretPadding = tooltip.caretPadding;
|
||||
var tooltipX, tooltipY, arrowX;
|
||||
var arrowMinIndent = 2 * tooltip.cornerRadius;
|
||||
var arrowSize = 5;
|
||||
|
||||
// Compute X position
|
||||
if ($(document).width() > 2 * tooltip.width || tooltip.xAlign !== "center") {
|
||||
// If the viewport is wide enough, let the tooltip follow the caret position
|
||||
tooltipX = offsetX + caretX;
|
||||
if (tooltip.yAlign === "top" || tooltip.yAlign === "bottom") {
|
||||
switch (tooltip.xAlign) {
|
||||
case "center":
|
||||
// set a minimal X position to 5px to prevent
|
||||
// the tooltip to stick out left of the viewport
|
||||
var minX = 5;
|
||||
if (2 * tooltipX < tooltipWidth + minX) {
|
||||
arrowX = tooltipX - minX;
|
||||
tooltipX = minX;
|
||||
} else {
|
||||
tooltipX -= tooltipWidth / 2;
|
||||
}
|
||||
|
||||
break;
|
||||
case "left":
|
||||
tooltipX -= arrowMinIndent;
|
||||
arrowX = arrowMinIndent;
|
||||
break;
|
||||
case "right":
|
||||
tooltipX -= tooltipWidth - arrowMinIndent;
|
||||
arrowX = tooltipWidth - arrowMinIndent;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (tooltip.yAlign === "center") {
|
||||
switch (tooltip.xAlign) {
|
||||
case "left":
|
||||
tooltipX += caretPadding;
|
||||
break;
|
||||
case "right":
|
||||
tooltipX -= tooltipWidth - caretPadding;
|
||||
break;
|
||||
case "center":
|
||||
tooltipX -= tooltipWidth / 2;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// compute the tooltip's center inside ancestor element
|
||||
tooltipX = (tooltipEl.ancestor.offsetWidth - tooltipWidth) / 2;
|
||||
// move the tooltip if the arrow would stick out to the left
|
||||
if (offsetX + caretX - arrowMinIndent < tooltipX) {
|
||||
tooltipX = offsetX + caretX - arrowMinIndent;
|
||||
}
|
||||
|
||||
// move the tooltip if the arrow would stick out to the right
|
||||
if (offsetX + caretX - tooltipWidth + arrowMinIndent > tooltipX) {
|
||||
tooltipX = offsetX + caretX - tooltipWidth + arrowMinIndent;
|
||||
}
|
||||
|
||||
arrowX = offsetX + caretX - tooltipX;
|
||||
}
|
||||
|
||||
// Prevent tooltip disapearing behind the sidebar
|
||||
if (tooltip.caretX < 100) {
|
||||
width = 100;
|
||||
// Compute Y position
|
||||
switch (tooltip.yAlign) {
|
||||
case "top":
|
||||
tooltipY = offsetY + caretY + arrowSize + caretPadding;
|
||||
break;
|
||||
case "center":
|
||||
tooltipY = offsetY + caretY - tooltipHeight / 2;
|
||||
if (tooltip.xAlign === "left") {
|
||||
tooltipX += arrowSize;
|
||||
} else if (tooltip.xAlign === "right") {
|
||||
tooltipX -= arrowSize;
|
||||
}
|
||||
|
||||
break;
|
||||
case "bottom":
|
||||
tooltipY = offsetY + caretY - tooltipHeight - arrowSize - caretPadding;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Position tooltip and display
|
||||
tooltipEl.style.top = tooltipY.toFixed(1) + "px";
|
||||
tooltipEl.style.left = tooltipX.toFixed(1) + "px";
|
||||
if (arrowX === undefined) {
|
||||
tooltipEl.querySelector(".arrow").style.left = "";
|
||||
} else {
|
||||
// Calculate percentage X value depending on the tooltip's
|
||||
// width to avoid hanging arrow out on tooltip width changes
|
||||
var arrowXpercent = ((100 / tooltipWidth) * arrowX).toFixed(1);
|
||||
tooltipEl.querySelector(".arrow").style.left = arrowXpercent + "%";
|
||||
}
|
||||
|
||||
tooltipEl.style.opacity = 1;
|
||||
tooltipEl.style.left = position.left + width + "px";
|
||||
tooltipEl.style.top = position.top + tooltip.caretY + window.pageYOffset + "px";
|
||||
tooltipEl.style.fontFamily = tooltip._bodyFontFamily;
|
||||
tooltipEl.style.fontSize = tooltip.bodyFontSize + "px";
|
||||
tooltipEl.style.fontStyle = tooltip._bodyFontStyle;
|
||||
tooltipEl.style.padding = tooltip.yPadding + "px " + tooltip.xPadding + "px";
|
||||
};
|
||||
|
||||
// Functions to update data in page
|
||||
@@ -160,8 +250,8 @@ function updateQueriesOverTime() {
|
||||
timeLineChart.data.labels.push(d);
|
||||
var blocked = data.ads_over_time[1][hour];
|
||||
var permitted = data.domains_over_time[1][hour] - blocked;
|
||||
timeLineChart.data.datasets[0].data.push(permitted);
|
||||
timeLineChart.data.datasets[1].data.push(blocked);
|
||||
timeLineChart.data.datasets[0].data.push(blocked);
|
||||
timeLineChart.data.datasets[1].data.push(permitted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,7 +662,7 @@ function updateTopLists() {
|
||||
$("#ad-frequency td").parent().remove();
|
||||
var domaintable = $("#domain-frequency").find("tbody:last");
|
||||
var adtable = $("#ad-frequency").find("tbody:last");
|
||||
var url, domain, percentage;
|
||||
var url, domain, percentage, urlText;
|
||||
for (domain in data.top_queries) {
|
||||
if (Object.prototype.hasOwnProperty.call(data.top_queries, domain)) {
|
||||
// Sanitize domain
|
||||
@@ -582,7 +672,8 @@ function updateTopLists() {
|
||||
}
|
||||
|
||||
domain = escapeHtml(domain);
|
||||
url = '<a href="queries.php?domain=' + domain + '">' + domain + "</a>";
|
||||
urlText = domain === "" ? "." : domain;
|
||||
url = '<a href="queries.php?domain=' + domain + '">' + urlText + "</a>";
|
||||
percentage = (data.top_queries[domain] / data.dns_queries_today) * 100;
|
||||
domaintable.append(
|
||||
"<tr> <td>" +
|
||||
@@ -614,7 +705,8 @@ function updateTopLists() {
|
||||
}
|
||||
|
||||
domain = escapeHtml(domain);
|
||||
url = '<a href="queries.php?domain=' + domain + '">' + domain + "</a>";
|
||||
urlText = domain === "" ? "." : domain;
|
||||
url = '<a href="queries.php?domain=' + domain + '">' + urlText + "</a>";
|
||||
percentage = (data.top_ads[domain] / data.ads_blocked_today) * 100;
|
||||
adtable.append(
|
||||
"<tr> <td>" +
|
||||
@@ -727,6 +819,9 @@ $(document).ready(function () {
|
||||
// Pull in data via AJAX
|
||||
updateSummaryData();
|
||||
|
||||
var blockedColor = "#999";
|
||||
var permittedColor = "#00a65a";
|
||||
|
||||
var ctx = document.getElementById("queryOverTimeChart").getContext("2d");
|
||||
timeLineChart = new Chart(ctx, {
|
||||
type: "bar",
|
||||
@@ -734,22 +829,22 @@ $(document).ready(function () {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: "Permitted DNS Queries",
|
||||
label: "Blocked DNS Queries",
|
||||
fill: true,
|
||||
backgroundColor: "rgba(0, 166, 90,.8)",
|
||||
borderColor: "rgba(0, 166, 90,.8)",
|
||||
pointBorderColor: "rgba(0, 166, 90,.8)",
|
||||
backgroundColor: blockedColor,
|
||||
borderColor: blockedColor,
|
||||
pointBorderColor: blockedColor,
|
||||
pointRadius: 1,
|
||||
pointHoverRadius: 5,
|
||||
data: [],
|
||||
pointHitRadius: 5
|
||||
},
|
||||
{
|
||||
label: "Blocked DNS Queries",
|
||||
label: "Permitted DNS Queries",
|
||||
fill: true,
|
||||
backgroundColor: "rgba(0,192,239,1)",
|
||||
borderColor: "rgba(0,192,239,1)",
|
||||
pointBorderColor: "rgba(0,192,239,1)",
|
||||
backgroundColor: permittedColor,
|
||||
borderColor: permittedColor,
|
||||
pointBorderColor: permittedColor,
|
||||
pointRadius: 1,
|
||||
pointHoverRadius: 5,
|
||||
data: [],
|
||||
@@ -761,6 +856,9 @@ $(document).ready(function () {
|
||||
tooltips: {
|
||||
enabled: true,
|
||||
mode: "x-axis",
|
||||
itemSort: function(a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
callbacks: {
|
||||
title: function (tooltipItem) {
|
||||
var label = tooltipItem[0].xLabel;
|
||||
@@ -769,13 +867,13 @@ $(document).ready(function () {
|
||||
var m = parseInt(time[2], 10) || 0;
|
||||
var from = padNumber(h) + ":" + padNumber(m - 5) + ":00";
|
||||
var to = padNumber(h) + ":" + padNumber(m + 4) + ":59";
|
||||
return "Upstreams from " + from + " to " + to;
|
||||
return "Queries from " + from + " to " + to;
|
||||
},
|
||||
label: function (tooltipItems, data) {
|
||||
if (tooltipItems.datasetIndex === 1) {
|
||||
label: function(tooltipItems, data) {
|
||||
if (tooltipItems.datasetIndex === 0) {
|
||||
var percentage = 0.0;
|
||||
var permitted = parseInt(data.datasets[0].data[tooltipItems.index]);
|
||||
var blocked = parseInt(data.datasets[1].data[tooltipItems.index]);
|
||||
var permitted = parseInt(data.datasets[1].data[tooltipItems.index]);
|
||||
var blocked = parseInt(data.datasets[0].data[tooltipItems.index]);
|
||||
var total = permitted + blocked;
|
||||
if (total > 0) {
|
||||
percentage = (100.0 * blocked) / total;
|
||||
@@ -844,7 +942,8 @@ $(document).ready(function () {
|
||||
enabled: false,
|
||||
mode: "x-axis",
|
||||
custom: customTooltips,
|
||||
itemSort: function (a, b) {
|
||||
yAlign: "top",
|
||||
itemSort: function(a, b) {
|
||||
return b.yLabel - a.yLabel;
|
||||
},
|
||||
callbacks: {
|
||||
@@ -888,7 +987,10 @@ $(document).ready(function () {
|
||||
}
|
||||
]
|
||||
},
|
||||
maintainAspectRatio: true
|
||||
maintainAspectRatio: false,
|
||||
hover: {
|
||||
animationDuration: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -998,3 +1100,8 @@ $(document).ready(function () {
|
||||
updateForwardDestinationsPie();
|
||||
}
|
||||
});
|
||||
|
||||
//destroy all chartjs customTooltips on window resize
|
||||
window.addEventListener("resize", function() {
|
||||
$(".chartjs-tooltip").remove();
|
||||
});
|
||||
|
||||
@@ -276,7 +276,8 @@ $(document).ready(function () {
|
||||
this.style.color = "";
|
||||
}
|
||||
);
|
||||
$("td:eq(4)", row).click(function () {
|
||||
$("td:eq(4)", row).off(); // Release any possible previous onClick event handlers
|
||||
$("td:eq(4)", row).click(function() {
|
||||
var new_tab = window.open("groups-domains.php?domainid=" + data[9], "_blank");
|
||||
if (new_tab) {
|
||||
new_tab.focus();
|
||||
@@ -286,11 +287,18 @@ $(document).ready(function () {
|
||||
$("td:eq(4)", row).addClass("pointer");
|
||||
}
|
||||
|
||||
// Add domain in CNAME chain causing the query to have been blocked
|
||||
// Substitute domain by "." if empty
|
||||
var domain = data[2];
|
||||
var CNAME_domain = data[8];
|
||||
if (domain.length === 0) {
|
||||
domain = ".";
|
||||
}
|
||||
|
||||
if (isCNAME) {
|
||||
var CNAME_domain = data[8];
|
||||
// Add domain in CNAME chain causing the query to have been blocked
|
||||
$("td:eq(2)", row).text(domain + "\n(blocked " + CNAME_domain + ")");
|
||||
} else {
|
||||
$("td:eq(2)", row).text(domain);
|
||||
}
|
||||
|
||||
// Check for existence of sixth column and display only if not Pi-holed
|
||||
|
||||
@@ -54,7 +54,9 @@ if ($_POST['action'] == 'get_groups') {
|
||||
} elseif ($_POST['action'] == 'add_group') {
|
||||
// Add new group
|
||||
try {
|
||||
$names = explode(' ', $_POST['name']);
|
||||
$names = explode(' ', trim($_POST['name']));
|
||||
$total = count($names);
|
||||
$added = 0;
|
||||
$stmt = $db->prepare('INSERT INTO "group" (name,description) VALUES (:name,:desc)');
|
||||
if (!$stmt) {
|
||||
throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
|
||||
@@ -66,12 +68,15 @@ if ($_POST['action'] == 'get_groups') {
|
||||
|
||||
foreach ($names as $name) {
|
||||
if (!$stmt->bindValue(':name', $name, SQLITE3_TEXT)) {
|
||||
throw new Exception('While binding name: ' . $db->lastErrorMsg());
|
||||
throw new Exception('While binding name: <strong>' . $db->lastErrorMsg() . '</strong><br>'.
|
||||
'Added ' . $added . " out of ". $total . " groups");
|
||||
}
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception('While executing: ' . $db->lastErrorMsg());
|
||||
throw new Exception('While executing: <strong>' . $db->lastErrorMsg() . '</strong><br>'.
|
||||
'Added ' . $added . " out of ". $total . " groups");
|
||||
}
|
||||
$added++;
|
||||
}
|
||||
|
||||
$reload = true;
|
||||
@@ -234,7 +239,9 @@ if ($_POST['action'] == 'get_groups') {
|
||||
} elseif ($_POST['action'] == 'add_client') {
|
||||
// Add new client
|
||||
try {
|
||||
$ips = explode(' ', $_POST['ip']);
|
||||
$ips = explode(' ', trim($_POST['ip']));
|
||||
$total = count($ips);
|
||||
$added = 0;
|
||||
$stmt = $db->prepare('INSERT INTO client (ip,comment) VALUES (:ip,:comment)');
|
||||
if (!$stmt) {
|
||||
throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
|
||||
@@ -251,12 +258,15 @@ if ($_POST['action'] == 'get_groups') {
|
||||
$comment = null;
|
||||
}
|
||||
if (!$stmt->bindValue(':comment', $comment, SQLITE3_TEXT)) {
|
||||
throw new Exception('While binding comment: ' . $db->lastErrorMsg());
|
||||
throw new Exception('While binding comment: <strong>' . $db->lastErrorMsg() . '</strong><br>'.
|
||||
'Added ' . $added . " out of ". $total . " clients");
|
||||
}
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception('While executing: ' . $db->lastErrorMsg());
|
||||
throw new Exception('While executing: <strong>' . $db->lastErrorMsg() . '</strong><br>'.
|
||||
'Added ' . $added . " out of ". $total . " clients");
|
||||
}
|
||||
$added++;
|
||||
}
|
||||
|
||||
$reload = true;
|
||||
@@ -391,11 +401,31 @@ if ($_POST['action'] == 'get_groups') {
|
||||
if (extension_loaded("intl") &&
|
||||
($res['type'] === ListType::whitelist ||
|
||||
$res['type'] === ListType::blacklist) ) {
|
||||
$utf8_domain = idn_to_utf8($res['domain']);
|
||||
|
||||
// Try to convert possible IDNA domain to Unicode, we try the UTS #46 standard first
|
||||
// as this is the new default, see https://sourceforge.net/p/icu/mailman/message/32980778/
|
||||
// We know that this fails for some Google domains violating the standard
|
||||
// see https://github.com/pi-hole/AdminLTE/issues/1223
|
||||
$utf8_domain = false;
|
||||
if (defined("INTL_IDNA_VARIANT_UTS46")) {
|
||||
// We have to use the option IDNA_NONTRANSITIONAL_TO_ASCII here
|
||||
// to ensure sparkasse-gießen.de is not converted into
|
||||
// sparkass-giessen.de but into xn--sparkasse-gieen-2ib.de
|
||||
// as mandated by the UTS #46 standard
|
||||
$utf8_domain = idn_to_utf8($res['domain'], IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
|
||||
}
|
||||
|
||||
// If conversion failed, try with the (deprecated!) IDNA 2003 variant
|
||||
// We have to check for its existance as support of this variant is
|
||||
// scheduled for removal with PHP 8.0
|
||||
// see https://wiki.php.net/rfc/deprecate-and-remove-intl_idna_variant_2003
|
||||
if ($utf8_domain === false && defined("INTL_IDNA_VARIANT_2003")) {
|
||||
$utf8_domain = idn_to_utf8($res['domain'], IDNA_DEFAULT, INTL_IDNA_VARIANT_2003);
|
||||
}
|
||||
|
||||
// Convert domain name to international form
|
||||
// if applicable and extension is available
|
||||
if($res['domain'] !== $utf8_domain)
|
||||
{
|
||||
if ($utf8_domain !== false && $res['domain'] !== $utf8_domain) {
|
||||
$res['domain'] = $utf8_domain.' ('.$res['domain'].')';
|
||||
}
|
||||
}
|
||||
@@ -410,7 +440,9 @@ if ($_POST['action'] == 'get_groups') {
|
||||
} elseif ($_POST['action'] == 'add_domain') {
|
||||
// Add new domain
|
||||
try {
|
||||
$domains = explode(' ', $_POST['domain']);
|
||||
$domains = explode(' ', trim($_POST['domain']));
|
||||
$total = count($domains);
|
||||
$added = 0;
|
||||
$stmt = $db->prepare('INSERT INTO domainlist (domain,type,comment) VALUES (:domain,:type,:comment)');
|
||||
if (!$stmt) {
|
||||
throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
|
||||
@@ -427,8 +459,22 @@ if ($_POST['action'] == 'get_groups') {
|
||||
}
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
$input = $domain;
|
||||
// Convert domain name to IDNA ASCII form for international domains
|
||||
$domain = idn_to_ascii($domain);
|
||||
if (extension_loaded("intl")) {
|
||||
// Be prepared that this may fail and see our comments above
|
||||
// (search for "idn_to_utf8)
|
||||
$idn_domain = false;
|
||||
if (defined("INTL_IDNA_VARIANT_UTS46")) {
|
||||
$idn_domain = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
|
||||
}
|
||||
if ($idn_domain === false && defined("INTL_IDNA_VARIANT_2003")) {
|
||||
$idn_domain = idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_2003);
|
||||
}
|
||||
if($idn_domain !== false) {
|
||||
$domain = $idn_domain;
|
||||
}
|
||||
}
|
||||
|
||||
if(strlen($_POST['type']) === 2 && $_POST['type'][1] === 'W')
|
||||
{
|
||||
@@ -442,17 +488,27 @@ if ($_POST['action'] == 'get_groups') {
|
||||
$domain = strtolower($domain);
|
||||
if(filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false)
|
||||
{
|
||||
throw new Exception('Domain ' . htmlentities(utf8_encode($domain)) . 'is not a valid domain.');
|
||||
// This is the case when idn_to_ascii() modified the string
|
||||
if($input !== $domain && strlen($domain) > 0)
|
||||
$errormsg = 'Domain ' . htmlentities($input) . ' (converted to "' . htmlentities(utf8_encode($domain)) . '") is not a valid domain.';
|
||||
elseif($input !== $domain)
|
||||
$errormsg = 'Domain ' . htmlentities($input) . ' is not a valid domain.';
|
||||
else
|
||||
$errormsg = 'Domain ' . htmlentities(utf8_encode($domain)) . ' is not a valid domain.';
|
||||
throw new Exception($errormsg . '<br>Added ' . $added . " out of ". $total . " domains");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$stmt->bindValue(':domain', $domain, SQLITE3_TEXT)) {
|
||||
throw new Exception('While binding domain: ' . $db->lastErrorMsg());
|
||||
throw new Exception('While binding domain: <strong>' . $db->lastErrorMsg() . '</strong><br>'.
|
||||
'Added ' . $added . " out of ". $total . " domains");
|
||||
}
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception('While executing: ' . $db->lastErrorMsg());
|
||||
throw new Exception('While executing: <strong>' . $db->lastErrorMsg() . '</strong><br>'.
|
||||
'Added ' . $added . " out of ". $total . " domains");
|
||||
}
|
||||
$added++;
|
||||
}
|
||||
|
||||
$reload = true;
|
||||
@@ -604,7 +660,9 @@ if ($_POST['action'] == 'get_groups') {
|
||||
} elseif ($_POST['action'] == 'add_adlist') {
|
||||
// Add new adlist
|
||||
try {
|
||||
$addresses = explode(' ', $_POST['address']);
|
||||
$addresses = explode(' ', trim($_POST['address']));
|
||||
$total = count($addresses);
|
||||
$added = 0;
|
||||
|
||||
$stmt = $db->prepare('INSERT INTO adlist (address,comment) VALUES (:address,:comment)');
|
||||
if (!$stmt) {
|
||||
@@ -616,17 +674,21 @@ if ($_POST['action'] == 'get_groups') {
|
||||
}
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
if(preg_match("/[^a-zA-Z0-9:\/?&%=~._-]/", $address) !== 0) {
|
||||
throw new Exception('Invalid adlist URL');
|
||||
if(preg_match("/[^a-zA-Z0-9:\/?&%=~._()-]/", $address) !== 0) {
|
||||
throw new Exception('<strong>Invalid adlist URL ' . htmlentities($address) . '</strong><br>'.
|
||||
'Added ' . $added . " out of ". $total . " adlists");
|
||||
}
|
||||
|
||||
if (!$stmt->bindValue(':address', $address, SQLITE3_TEXT)) {
|
||||
throw new Exception('While binding address: ' . $db->lastErrorMsg());
|
||||
throw new Exception('While binding address: <strong>' . $db->lastErrorMsg() . '</strong><br>'.
|
||||
'Added ' . $added . " out of ". $total . " adlists");
|
||||
}
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception('While executing: ' . $db->lastErrorMsg());
|
||||
throw new Exception('While executing: <strong>' . $db->lastErrorMsg() . '</strong><br>'.
|
||||
'Added ' . $added . " out of ". $total . " adlists");
|
||||
}
|
||||
$added++;
|
||||
}
|
||||
|
||||
$reload = true;
|
||||
|
||||
@@ -215,8 +215,8 @@
|
||||
<script src="scripts/vendor/bootstrap-notify.min.js"></script>
|
||||
|
||||
<?php if(in_array($scriptname, array("groups.php", "groups-clients.php", "groups-domains.php", "groups-adlists.php"))){ ?>
|
||||
<script src="style/vendor/bootstrap/js/bootstrap-multiselect.js"></script>
|
||||
<link rel="stylesheet" href="style/vendor/bootstrap/css/bootstrap-multiselect.css">
|
||||
<script src="style/vendor/bootstrap/js/bootstrap-select.min.js"></script>
|
||||
<link rel="stylesheet" href="style/vendor/bootstrap/css/bootstrap-select.min.css">
|
||||
<script src="style/vendor/bootstrap/js/bootstrap-toggle.min.js"></script>
|
||||
<link rel="stylesheet" href="style/vendor/bootstrap/css/bootstrap-toggle.min.css">
|
||||
<script src="scripts/vendor/moment.min.js"></script>
|
||||
@@ -318,7 +318,7 @@ if($auth) {
|
||||
*/ ?>
|
||||
<!-- PayPal -->
|
||||
<div class="text-center">
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3J2L3Z4DHW9UY" rel="noopener" target="_blank" style="background:none">
|
||||
<a href="https://pi-hole.net/donate/" rel="noopener" target="_blank" style="background:none">
|
||||
<img src="img/donate.gif" alt="Donate">
|
||||
</a>
|
||||
</div>
|
||||
@@ -623,10 +623,10 @@ if($auth) {
|
||||
<i class="fa fa-cogs"></i> <span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Custom DNS -->
|
||||
<li<?php if($scriptname === "custom_dns.php"){ ?> class="active"<?php } ?>>
|
||||
<a href="custom_dns.php">
|
||||
<i class="fa fa-address-book"></i> <span>Custom DNS</span>
|
||||
<!-- Local DNS Records -->
|
||||
<li<?php if($scriptname === "dns_records.php"){ ?> class="active"<?php } ?>>
|
||||
<a href="dns_records.php">
|
||||
<i class="fa fa-address-book"></i> <span>Local DNS Records</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Logout -->
|
||||
@@ -652,7 +652,7 @@ if($auth) {
|
||||
<?php } ?>
|
||||
<!-- Donate -->
|
||||
<li>
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3J2L3Z4DHW9UY" rel="noopener" target="_blank">
|
||||
<a href="https://pi-hole.net/donate/" rel="noopener" target="_blank">
|
||||
<i class="fa-paypal-icon fab fa-paypal"></i> <span>Donate</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -79,16 +79,10 @@ function readStaticLeasesFile($origin_file="/etc/dnsmasq.d/04-pihole-static-dhcp
|
||||
{
|
||||
global $dhcp_static_leases;
|
||||
$dhcp_static_leases = array();
|
||||
try
|
||||
{
|
||||
$dhcpstatic = @fopen($origin_file, 'r');
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
echo "Warning: Failed to read ".$origin_file.", this is not an error";
|
||||
if(!file_exists($origin_file) || !is_readable($origin_file))
|
||||
return false;
|
||||
}
|
||||
|
||||
$dhcpstatic = @fopen($origin_file, 'r');
|
||||
if(!is_resource($dhcpstatic))
|
||||
return false;
|
||||
|
||||
@@ -154,16 +148,16 @@ function readDNSserversList()
|
||||
$line = explode(';', $line);
|
||||
$name = $line[0];
|
||||
$values = [];
|
||||
if (!empty($line[1])) {
|
||||
if (!empty($line[1]) && validIP($line[1])) {
|
||||
$values["v4_1"] = $line[1];
|
||||
}
|
||||
if (!empty($line[2])) {
|
||||
if (!empty($line[2]) && validIP($line[2])) {
|
||||
$values["v4_2"] = $line[2];
|
||||
}
|
||||
if (!empty($line[3])) {
|
||||
if (!empty($line[3]) && validIP($line[3])) {
|
||||
$values["v6_1"] = $line[3];
|
||||
}
|
||||
if (!empty($line[4])) {
|
||||
if (!empty($line[4]) && validIP($line[4])) {
|
||||
$values["v6_2"] = $line[4];
|
||||
}
|
||||
$list[$name] = $values;
|
||||
|
||||
@@ -688,6 +688,8 @@ if (isset($_GET['tab']) && in_array($_GET['tab'], array("sysadmin", "blocklists"
|
||||
<input type="text" name="custom1val" class="form-control" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off"
|
||||
<?php if (isset($custom1)){ ?>value="<?php echo $custom1; ?>"<?php } ?>>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Custom 2 (IPv4)</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
@@ -710,6 +712,8 @@ if (isset($_GET['tab']) && in_array($_GET['tab'], array("sysadmin", "blocklists"
|
||||
<input type="text" name="custom3val" class="form-control" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off"
|
||||
<?php if (isset($custom3)){ ?>value="<?php echo $custom3; ?>"<?php } ?>>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Custom 4 (IPv6)</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
@@ -969,10 +973,15 @@ if (isset($_GET['tab']) && in_array($_GET['tab'], array("sysadmin", "blocklists"
|
||||
</div>
|
||||
<h4>Administrator Email Address</h4>
|
||||
<div class="form-group">
|
||||
<<<<<<< HEAD
|
||||
<div class="input-group">
|
||||
<input type="email" class="form-control" name="adminemail"
|
||||
value="<?php echo htmlspecialchars($adminemail); ?>">
|
||||
</div>
|
||||
=======
|
||||
<input type="text" class="form-control" name="adminemail"
|
||||
value="<?php echo htmlspecialchars($adminemail); ?>">
|
||||
>>>>>>> master
|
||||
</div>
|
||||
<input type="hidden" name="field" value="webUI">
|
||||
<input type="hidden" name="token" value="<?php echo $token ?>">
|
||||
|
||||
+66
-15
@@ -157,27 +157,72 @@
|
||||
user-select: none
|
||||
}
|
||||
|
||||
#chartjs-tooltip {
|
||||
opacity: 1;
|
||||
.chartjs-tooltip {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, .7);
|
||||
color: #fff;
|
||||
border-radius: 3px;
|
||||
-webkit-transition: all .1s ease;
|
||||
-o-transition: all .1s ease;
|
||||
transition: all .1s ease;
|
||||
pointer-events: none;
|
||||
-webkit-transform: translate(-50%, 0);
|
||||
-ms-transform: translate(-50%, 0);
|
||||
-o-transform: translate(-50%, 0);
|
||||
transform: translate(-50%, 0);
|
||||
color: #fff;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
max-width: 95%;
|
||||
z-index: 900;
|
||||
}
|
||||
|
||||
.chartjs-tooltip th {
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.chartjs-tooltip,
|
||||
.chartjs-tooltip .arrow {
|
||||
-webkit-transition: all .2s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
transition: all .2s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
}
|
||||
|
||||
.chartjs-tooltip .arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.chartjs-tooltip.bottom .arrow {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin: 0 -5px -5px;
|
||||
border-width: 5px 5px 0;
|
||||
border-top-color: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.chartjs-tooltip.top .arrow {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
border-width: 0 5px 5px;
|
||||
margin: -5px -5px 0;
|
||||
border-bottom-color: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.chartjs-tooltip.right.center .arrow {
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
margin: -5px -5px -5px 0;
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-left-color: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.chartjs-tooltip.left.center .arrow {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
margin: -5px 0 -5px -5px;
|
||||
border-width: 5px 5px 5px 0;
|
||||
border-right-color: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.chartjs-tooltip-key {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 10px;
|
||||
margin-right: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 3px;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
@@ -191,6 +236,7 @@
|
||||
|
||||
.chart-legend li {
|
||||
cursor: pointer;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.chart-legend li span {
|
||||
@@ -259,4 +305,9 @@ code.breakall
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bootstrap-select.bs-container.align-right {
|
||||
left: unset !important;
|
||||
right: 10px;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
span.multiselect-native-select{position:relative}span.multiselect-native-select select{border:0!important;clip:rect(0 0 0 0)!important;height:1px!important;margin:-1px -1px -1px -3px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;left:50%;top:30px}.multiselect-container{position:absolute;list-style-type:none;margin:0;padding:0}.multiselect-container .input-group{margin:5px}.multiselect-container>li{padding:0}.multiselect-container>li>a.multiselect-all label{font-weight:700}.multiselect-container>li.multiselect-group label{margin:0;padding:3px 20px 3px 20px;height:100%;font-weight:700}.multiselect-container>li.multiselect-group-clickable label{cursor:pointer}.multiselect-container>li>a{padding:0}.multiselect-container>li>a>label{margin:0;height:100%;cursor:pointer;font-weight:400;padding:3px 20px 3px 40px}.multiselect-container>li>a>label.radio,.multiselect-container>li>a>label.checkbox{margin:0}.multiselect-container>li>a>label>input[type=checkbox]{margin-bottom:5px}.btn-group>.btn-group:nth-child(2)>.multiselect.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.form-inline .multiselect-container label.checkbox,.form-inline .multiselect-container label.radio{padding:3px 20px 3px 40px}.form-inline .multiselect-container li a label.checkbox input[type=checkbox],.form-inline .multiselect-container li a label.radio input[type=radio]{margin-left:-20px;margin-right:0}
|
||||
File diff suppressed because one or more lines are too long
-1716
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user