mirror of
https://github.com/pi-hole/web.git
synced 2025-12-24 20:55:28 +00:00
Merge branch 'development-v6' into new/loading
Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
7
.devcontainer/Dockerfile
Normal file
7
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM node:21-alpine3.18
|
||||
RUN apk add --no-cache \
|
||||
git \
|
||||
nano\
|
||||
openssh
|
||||
|
||||
USER node
|
||||
23
.devcontainer/devcontainer.json
Normal file
23
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "Pi-hole web devcontainer",
|
||||
"dockerFile": "Dockerfile",
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "npm install",
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
"settings": {},
|
||||
"extensions": [
|
||||
"eamodio.gitlens",
|
||||
"EditorConfig.EditorConfig",
|
||||
"github.vscode-github-actions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"containerEnv": {
|
||||
"GIT_EDITOR": "nano"
|
||||
},
|
||||
"mounts": [
|
||||
"type=bind,source=/home/${localEnv:USER}/.ssh,target=/home/node/.ssh,readonly"
|
||||
]
|
||||
}
|
||||
@@ -17,7 +17,7 @@ indent_size = 2
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
[package.json]
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
[.yamllint.conf]
|
||||
|
||||
3
.github/workflows/codespell.yml
vendored
3
.github/workflows/codespell.yml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
|
||||
26
error403.lp
Normal file
26
error403.lp
Normal file
@@ -0,0 +1,26 @@
|
||||
<? --[[
|
||||
* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2023 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.
|
||||
--]]
|
||||
|
||||
mg.include('scripts/pi-hole/lua/header.lp','r')
|
||||
?>
|
||||
<body class="hold-transition layout-boxed login-page">
|
||||
<div class="box login-box">
|
||||
<section style="padding: 15px;">
|
||||
<h2 class="error-headline text-danger">403</h2>
|
||||
<div class="error-content">
|
||||
<h3><i class="fa fa-times-circle text-danger"></i> Oops! Access denied.</h3>
|
||||
<p>
|
||||
You don't have permission to access <code><?=mg.request_info.request_uri?></code> on this server.<br>
|
||||
Did you mean to go to <a href="<?=pihole.webhome()?>">your Pi-hole's dashboard</a> instead?
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -41,7 +41,8 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>You can select an existing client or add a custom one by typing into the field above and confirming your entry with <kbd>⏎</kbd>.</p>
|
||||
<p>You can select an existing client or add a custom one by typing into the field above and confirming your entry with <kbd>⏎</kbd>.
|
||||
Multiple clients can be added by separating each client with a space or comma.</p>
|
||||
<p>Clients may be described either by their IP addresses (IPv4 and IPv6 are supported),
|
||||
IP subnets (CIDR notation, like <code>192.168.2.0/24</code>),
|
||||
their MAC addresses (like <code>12:34:56:78:9A:BC</code>),
|
||||
|
||||
@@ -86,7 +86,8 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
<div>
|
||||
<p><strong>Note:</strong><br>
|
||||
The domain or regex filter will be automatically assigned to the Default Group.<br>
|
||||
Other groups can optionally be assigned in the list below (using <b>Group assignment</b>).
|
||||
Other groups can optionally be assigned in the list below (using <strong>Group assignment</strong>).<br>
|
||||
You can add multiple entries at once by separating them with spaces (e.g. <code>example.com example.org</code>).
|
||||
</p>
|
||||
</div>
|
||||
<div class="btn-toolbar pull-right" role="toolbar" aria-label="Toolbar with buttons">
|
||||
|
||||
@@ -30,7 +30,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="new_address">Address:</label>
|
||||
<input id="new_address" type="text" class="form-control" placeholder="URL or space-separated URLs" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
<input id="new_address" type="text" class="form-control" placeholder="URL" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="new_comment">Comment:</label>
|
||||
@@ -42,7 +42,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
<strong>Hints:</strong>
|
||||
<ol>
|
||||
<li>Please run <code>pihole -g</code> or update your gravity list <a href="<?=webhome?>gravity">online</a> after modifying your lists.</li>
|
||||
<li>Multiple lists can be added by separating each <i>unique</i> URL with a space</li>
|
||||
<li>Multiple lists can be added by separating each <i>unique</i> URL with a space or comma</li>
|
||||
<li>Click on the icon in the first column to get additional information about your lists. The icons correspond to the health of the list.</li>
|
||||
</ol>
|
||||
<div class="btn-toolbar pull-right" role="toolbar" aria-label="Toolbar with buttons">
|
||||
|
||||
@@ -30,7 +30,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
<div class="row">
|
||||
<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 or space-separated group names">
|
||||
<input id="new_name" type="text" class="form-control" placeholder="Group name">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="new_comment">Comment:</label>
|
||||
@@ -41,7 +41,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
<div class="box-footer clearfix">
|
||||
<strong>Hints:</strong>
|
||||
<ol>
|
||||
<li>Multiple groups can be added by separating each group name with a space</li>
|
||||
<li>Multiple groups can be added by separating each group name with a space or comma</li>
|
||||
<li>Group names can have spaces if entered in quotes. e.g "My New Group"</li>
|
||||
</ol>
|
||||
<button type="button" id="btnAdd" class="btn btn-primary pull-right">Add</button>
|
||||
|
||||
1991
package-lock.json
generated
1991
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@
|
||||
"eslint-plugin-compat": "^4.2.0",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-cli": "^10.1.0",
|
||||
"prettier": "3.0.3",
|
||||
"prettier": "^3.1.0",
|
||||
"xo": "^0.56.0"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global upstreams */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var THEME_COLORS = [
|
||||
"#f56954",
|
||||
@@ -97,7 +99,8 @@ const htmlLegendPlugin = {
|
||||
window.location.href = "queries.lp?type=" + item.text;
|
||||
} else if (chart.canvas.id === "forwardDestinationPieChart") {
|
||||
// Encode the forward destination as it may contain an "#" character
|
||||
window.location.href = "queries.lp?upstream=" + encodeURIComponent(item.text);
|
||||
const upstream = encodeURIComponent(upstreams[item.text]);
|
||||
window.location.href = "queries.lp?upstream=" + upstream;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,24 @@
|
||||
|
||||
var settingsLevel = 0;
|
||||
|
||||
const REFRESH_INTERVAL = {
|
||||
logs: 500, // 0.5 sec (logs page)
|
||||
summary: 1000, // 1 sec (dashboard)
|
||||
blocking: 10000, // 10 sec (all pages, sidebar)
|
||||
metrics: 10000, // 10 sec (settings page)
|
||||
system: 20000, // 20 sec (all pages, sidebar)
|
||||
sensors: 20000, // 20 sec (all pages, sidebar)
|
||||
query_types: 60000, // 1 min (dashboard)
|
||||
upstreams: 60000, // 1 min (dashboard)
|
||||
top_lists: 60000, // 1 min (dashboard)
|
||||
messages: 60000, // 1 min (all pages)
|
||||
version: 120000, // 2 min (all pages, footer)
|
||||
ftl: 120000, // 2 min (all pages, sidebar)
|
||||
hosts: 120000, // 2 min (settings page)
|
||||
history: 600000, // 10 min (dashboard)
|
||||
clients: 600000, // 10 min (dashboard)
|
||||
};
|
||||
|
||||
function secondsTimeSpanToHMS(s) {
|
||||
var h = Math.floor(s / 3600); //Get whole hours
|
||||
s -= h * 3600;
|
||||
@@ -86,17 +104,23 @@ function countDown() {
|
||||
}
|
||||
|
||||
function checkBlocking() {
|
||||
// Skip if page is hidden
|
||||
if (document.hidden) {
|
||||
utils.setTimer(checkBlocking, REFRESH_INTERVAL.blocking);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/api/dns/blocking",
|
||||
method: "GET",
|
||||
})
|
||||
.done(function (data) {
|
||||
piholeChanged(data.blocking);
|
||||
setTimeout(checkBlocking, 10000);
|
||||
utils.setTimer(checkBlocking, REFRESH_INTERVAL.blocking);
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
setTimeout(checkBlocking, 30000);
|
||||
utils.setTimer(checkBlocking, 3 * REFRESH_INTERVAL.blocking);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,6 +145,7 @@ function piholeChange(action, duration) {
|
||||
method: "POST",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
blocking: action === "enable",
|
||||
timer: parseInt(duration, 10) > 0 ? parseInt(duration, 10) : null,
|
||||
@@ -240,9 +265,8 @@ function updateFtlInfo() {
|
||||
ftl.allow_destructive ? "" : "Destructive actions are disabled by a config setting"
|
||||
);
|
||||
|
||||
// Update every 120 seconds
|
||||
clearTimeout(ftlinfoTimer);
|
||||
ftlinfoTimer = setTimeout(updateFtlInfo, 120000);
|
||||
ftlinfoTimer = utils.setTimer(updateFtlInfo, REFRESH_INTERVAL.ftl);
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
@@ -339,9 +363,9 @@ function updateSystemInfo() {
|
||||
moment.duration(1000 * system.uptime).humanize() + " (running since " + startdate + ")"
|
||||
);
|
||||
$("#sysinfo-system-overlay").hide();
|
||||
// Update every 20 seconds
|
||||
|
||||
clearTimeout(systemTimer);
|
||||
systemTimer = setTimeout(updateSystemInfo, 20000);
|
||||
systemTimer = utils.setTimer(updateSystemInfo, REFRESH_INTERVAL.system);
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
@@ -394,7 +418,7 @@ function updateSensorsInfo() {
|
||||
|
||||
// Update every 20 seconds
|
||||
clearTimeout(sensorsTimer);
|
||||
sensorsTimer = setTimeout(updateSensorsInfo, 20000);
|
||||
sensorsTimer = utils.setTimer(updateSensorsInfo, REFRESH_INTERVAL.sensors);
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
@@ -563,9 +587,8 @@ function updateVersionInfo() {
|
||||
'To install updates, run <code><a href="https://docs.pi-hole.net/main/update/" rel="noopener" target="_blank">pihole -up</a></code>.'
|
||||
);
|
||||
|
||||
// Update every 120 seconds
|
||||
clearTimeout(versionTimer);
|
||||
versionTimer = setTimeout(updateVersionInfo, 120000);
|
||||
versionTimer = utils.setTimer(updateVersionInfo, REFRESH_INTERVAL.version);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -588,8 +611,8 @@ $(function () {
|
||||
if (window.location.pathname !== "/admin/login") {
|
||||
// Run check immediately after page loading ...
|
||||
utils.checkMessages();
|
||||
// ... and once again with five seconds delay
|
||||
setTimeout(utils.checkMessages, 5000);
|
||||
// ... and then periodically
|
||||
utils.setInter(utils.checkMessages, REFRESH_INTERVAL.messages);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, groups:false,, apiFailure:false, updateFtlInfo:false, getGroups:false, processGroupResult:false */
|
||||
/* exported initTable */
|
||||
|
||||
var table;
|
||||
|
||||
@@ -82,7 +83,7 @@ $(function () {
|
||||
|
||||
reloadClientSuggestions();
|
||||
utils.setBsSelectDefaults();
|
||||
initTable();
|
||||
getGroups();
|
||||
|
||||
$("#select").on("change", function () {
|
||||
$("#ip-custom").val("");
|
||||
@@ -90,231 +91,228 @@ $(function () {
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function initTable() {
|
||||
table = $("#clientsTable")
|
||||
.on("preXhr.dt", function () {
|
||||
getGroups();
|
||||
})
|
||||
.DataTable({
|
||||
processing: true,
|
||||
ajax: {
|
||||
url: "/api/clients",
|
||||
dataSrc: "clients",
|
||||
type: "GET",
|
||||
table = $("#clientsTable").DataTable({
|
||||
processing: true,
|
||||
ajax: {
|
||||
url: "/api/clients",
|
||||
dataSrc: "clients",
|
||||
type: "GET",
|
||||
},
|
||||
order: [[0, "asc"]],
|
||||
columns: [
|
||||
{ data: "id", visible: false },
|
||||
{ data: null, visible: true, orderable: false, width: "15px" },
|
||||
{ data: "client", type: "ip-address" },
|
||||
{ data: "comment" },
|
||||
{ data: "groups", searchable: false },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
],
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 1,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
order: [[0, "asc"]],
|
||||
columns: [
|
||||
{ data: "id", visible: false },
|
||||
{ data: null, visible: true, orderable: false, width: "15px" },
|
||||
{ data: "client", type: "ip-address" },
|
||||
{ data: "comment" },
|
||||
{ data: "groups", searchable: false },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
],
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 1,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
{
|
||||
targets: "_all",
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
// Hide buttons if all clients were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
{
|
||||
targets: "_all",
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
// Hide buttons if all clients were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
$('button[id^="deleteClient_"]').on("click", deleteClient);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var dataId = utils.hexEncode(data.client);
|
||||
$(row).attr("data-id", dataId);
|
||||
var tooltip =
|
||||
"Added: " +
|
||||
utils.datetime(data.date_added, false) +
|
||||
"\nLast modified: " +
|
||||
utils.datetime(data.date_modified, false) +
|
||||
"\nDatabase ID: " +
|
||||
data.id;
|
||||
var ipName =
|
||||
'<code id="ip_' +
|
||||
$('button[id^="deleteClient_"]').on("click", deleteClient);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var dataId = utils.hexEncode(data.client);
|
||||
$(row).attr("data-id", dataId);
|
||||
var tooltip =
|
||||
"Added: " +
|
||||
utils.datetime(data.date_added, false) +
|
||||
"\nLast modified: " +
|
||||
utils.datetime(data.date_modified, false) +
|
||||
"\nDatabase ID: " +
|
||||
data.id;
|
||||
var ipName =
|
||||
'<code id="ip_' +
|
||||
dataId +
|
||||
'" title="' +
|
||||
tooltip +
|
||||
'" class="breakall">' +
|
||||
utils.escapeHtml(data.client) +
|
||||
"</code>";
|
||||
if (data.name !== null && data.name.length > 0)
|
||||
ipName +=
|
||||
'<br><code id="name_' +
|
||||
dataId +
|
||||
'" title="' +
|
||||
tooltip +
|
||||
'" class="breakall">' +
|
||||
data.client +
|
||||
utils.escapeHtml(data.name) +
|
||||
"</code>";
|
||||
if (data.name !== null && data.name.length > 0)
|
||||
ipName +=
|
||||
'<br><code id="name_' +
|
||||
dataId +
|
||||
'" title="' +
|
||||
tooltip +
|
||||
'" class="breakall">' +
|
||||
data.name +
|
||||
"</code>";
|
||||
$("td:eq(1)", row).html(ipName);
|
||||
$("td:eq(1)", row).html(ipName);
|
||||
|
||||
$("td:eq(2)", row).html('<input id="comment_' + dataId + '" class="form-control">');
|
||||
var commentEl = $("#comment_" + dataId, row);
|
||||
commentEl.val(utils.unescapeHtml(data.comment));
|
||||
commentEl.on("change", editClient);
|
||||
$("td:eq(2)", row).html('<input id="comment_' + dataId + '" class="form-control">');
|
||||
var commentEl = $("#comment_" + dataId, row);
|
||||
commentEl.val(data.comment);
|
||||
commentEl.on("change", editClient);
|
||||
|
||||
$("td:eq(3)", row).empty();
|
||||
$("td:eq(3)", row).append(
|
||||
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
|
||||
$("td:eq(3)", row).empty();
|
||||
$("td:eq(3)", row).append(
|
||||
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
|
||||
);
|
||||
var selectEl = $("#multiselect_" + dataId, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var dataSub = "";
|
||||
if (!groups[i].enabled) {
|
||||
dataSub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
|
||||
selectEl.append(
|
||||
$("<option " + dataSub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name)
|
||||
);
|
||||
var selectEl = $("#multiselect_" + dataId, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var dataSub = "";
|
||||
if (!groups[i].enabled) {
|
||||
dataSub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
})
|
||||
.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_' +
|
||||
dataId +
|
||||
' class="btn btn-block btn-sm" disabled>Apply</button>'
|
||||
);
|
||||
|
||||
selectEl.append(
|
||||
$("<option " + dataSub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name)
|
||||
);
|
||||
}
|
||||
var applyBtn = "#btn_apply_" + dataId;
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
})
|
||||
.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_' +
|
||||
dataId +
|
||||
' class="btn btn-block btn-sm" disabled>Apply</button>'
|
||||
);
|
||||
|
||||
var applyBtn = "#btn_apply_" + dataId;
|
||||
|
||||
var button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteClient_' +
|
||||
dataId +
|
||||
'" data-id="' +
|
||||
dataId +
|
||||
'">' +
|
||||
'<span class="far fa-trash-alt"></span>' +
|
||||
"</button>";
|
||||
$("td:eq(4)", row).html(button);
|
||||
},
|
||||
select: {
|
||||
style: "multi",
|
||||
selector: "td:not(:last-child)",
|
||||
info: false,
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
var button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteClient_' +
|
||||
dataId +
|
||||
'" data-id="' +
|
||||
dataId +
|
||||
'">' +
|
||||
'<span class="far fa-trash-alt"></span>' +
|
||||
"</button>";
|
||||
$("td:eq(4)", row).html(button);
|
||||
},
|
||||
select: {
|
||||
style: "multi",
|
||||
selector: "td:not(:last-child)",
|
||||
info: false,
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
{
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "selectNone",
|
||||
text: '<span class="far fa-check-square"></span>',
|
||||
titleAttr: "Deselect All",
|
||||
className: "btn-sm datatable-bt removeAll",
|
||||
},
|
||||
{
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push($(this).attr("data-id"));
|
||||
});
|
||||
// Delete all selected rows at once
|
||||
delItems(ids);
|
||||
},
|
||||
},
|
||||
],
|
||||
dom:
|
||||
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'i>>",
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"],
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
utils.stateSaveCallback("groups-clients-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("groups-clients-table");
|
||||
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reset visibility of ID column
|
||||
data.columns[0].visible = false;
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
{
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
extend: "selectNone",
|
||||
text: '<span class="far fa-check-square"></span>',
|
||||
titleAttr: "Deselect All",
|
||||
className: "btn-sm datatable-bt removeAll",
|
||||
},
|
||||
{
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push($(this).attr("data-id"));
|
||||
});
|
||||
// Delete all selected rows at once
|
||||
delItems(ids);
|
||||
},
|
||||
},
|
||||
],
|
||||
dom:
|
||||
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'i>>",
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"],
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
utils.stateSaveCallback("groups-clients-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("groups-clients-table");
|
||||
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reset visibility of ID column
|
||||
data.columns[0].visible = false;
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
// Disable autocorrect in the search box
|
||||
var input = document.querySelector("input[type=search]");
|
||||
@@ -405,11 +403,14 @@ function delItems(ids) {
|
||||
}
|
||||
|
||||
function addClient() {
|
||||
const comment = utils.escapeHtml($("#new_comment").val());
|
||||
const comment = $("#new_comment").val();
|
||||
|
||||
// Check if the user wants to add multiple IPs (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
var ips = utils.escapeHtml($("#select").val().trim()).split(/[\s,]+/);
|
||||
var ips = $("#select")
|
||||
.val()
|
||||
.trim()
|
||||
.split(/[\s,]+/);
|
||||
// Remove empty elements
|
||||
ips = ips.filter(function (el) {
|
||||
return el !== "";
|
||||
@@ -454,11 +455,13 @@ function addClient() {
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ client: ips, comment: comment }),
|
||||
success: function (data) {
|
||||
utils.enableAll();
|
||||
utils.listsAlert("client", ips, data);
|
||||
reloadClientSuggestions();
|
||||
$("#new_comment").val("");
|
||||
table.ajax.reload(null, false);
|
||||
table.rows().deselect();
|
||||
|
||||
@@ -483,7 +486,7 @@ function editClient() {
|
||||
.find("#multiselect_" + client)
|
||||
.val()
|
||||
.map(Number);
|
||||
const comment = utils.escapeHtml(tr.find("#comment_" + client).val());
|
||||
const comment = tr.find("#comment_" + client).val();
|
||||
const enabled = tr.find("#enabled_" + client).is(":checked");
|
||||
|
||||
var done = "edited";
|
||||
@@ -520,6 +523,7 @@ function editClient() {
|
||||
method: "put",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
client: client,
|
||||
groups: groups,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global apiFailure:false, utils:false */
|
||||
/* global apiFailure:false, utils:false, initTable:false */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var groups = [];
|
||||
@@ -18,6 +18,8 @@ function getGroups() {
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
groups = data.groups;
|
||||
// Actually load table contents
|
||||
initTable();
|
||||
},
|
||||
error: function (data) {
|
||||
apiFailure(data);
|
||||
@@ -34,11 +36,6 @@ function processGroupResult(data, type, done, notDone) {
|
||||
// Loop over errors and display them
|
||||
data.processed.errors.forEach(function (error) {
|
||||
console.log(error); // eslint-disable-line no-console
|
||||
utils.showAlert(
|
||||
"error",
|
||||
"",
|
||||
`Error while ${notDone} ${type} ${utils.escapeHtml(error.item)}`,
|
||||
error.error
|
||||
);
|
||||
utils.showAlert("error", "", `Error while ${notDone} ${type} ${error.item}`, error.error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, groups:false,, getGroups:false, updateFtlInfo:false, apiFailure:false, processGroupResult:false */
|
||||
/* exported initTable */
|
||||
|
||||
var table;
|
||||
var GETDict = {};
|
||||
@@ -44,7 +45,7 @@ $(function () {
|
||||
});
|
||||
|
||||
utils.setBsSelectDefaults();
|
||||
initTable();
|
||||
getGroups();
|
||||
});
|
||||
|
||||
// Show a list of suggested domains based on the user's input
|
||||
@@ -91,294 +92,292 @@ function hideSuggestDomains() {
|
||||
$("#suggest_domains").slideUp("fast");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function initTable() {
|
||||
table = $("#domainsTable")
|
||||
.on("preXhr.dt", function () {
|
||||
getGroups();
|
||||
})
|
||||
.DataTable({
|
||||
processing: true,
|
||||
ajax: {
|
||||
url: "/api/domains",
|
||||
dataSrc: "domains",
|
||||
type: "GET",
|
||||
},
|
||||
order: [[0, "asc"]],
|
||||
columns: [
|
||||
{ data: "id", visible: false },
|
||||
{ data: null, visible: true, orderable: false, width: "15px" },
|
||||
{ data: "domain" },
|
||||
{ data: "type", searchable: false },
|
||||
{ data: "enabled", searchable: false },
|
||||
{ data: "comment" },
|
||||
{ data: "groups", searchable: false },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
],
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 1,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
return "";
|
||||
},
|
||||
table = $("#domainsTable").DataTable({
|
||||
processing: true,
|
||||
ajax: {
|
||||
url: "/api/domains",
|
||||
dataSrc: "domains",
|
||||
type: "GET",
|
||||
},
|
||||
order: [[0, "asc"]],
|
||||
columns: [
|
||||
{ data: "id", visible: false },
|
||||
{ data: null, visible: true, orderable: false, width: "15px" },
|
||||
{ data: "domain" },
|
||||
{ data: "type", searchable: false },
|
||||
{ data: "enabled", searchable: false },
|
||||
{ data: "comment" },
|
||||
{ data: "groups", searchable: false },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
],
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 1,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
return "";
|
||||
},
|
||||
{
|
||||
targets: "_all",
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
// Hide buttons if all domains were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
$('button[id^="deleteDomain_"]').on("click", deleteDomain);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var dataId = utils.hexEncode(data.domain) + "_" + data.type + "_" + data.kind;
|
||||
$(row).attr("data-id", dataId);
|
||||
// Tooltip for domain
|
||||
var tooltip =
|
||||
"Added: " +
|
||||
utils.datetime(data.date_added, false) +
|
||||
"\nLast modified: " +
|
||||
utils.datetime(data.date_modified, false) +
|
||||
"\nDatabase ID: " +
|
||||
data.id;
|
||||
$("td:eq(1)", row).html(
|
||||
'<code id="domain_' +
|
||||
dataId +
|
||||
'" title="' +
|
||||
tooltip +
|
||||
'" class="breakall">' +
|
||||
data.domain +
|
||||
"</code>"
|
||||
);
|
||||
{
|
||||
targets: "_all",
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
// Hide buttons if all domains were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
// Drop-down type selector
|
||||
$("td:eq(2)", row).html(
|
||||
'<select id="type_' +
|
||||
dataId +
|
||||
'" class="form-control">' +
|
||||
'<option value="allow/exact"' +
|
||||
(data.type === "allow" && data.kind === "exact" ? " selected" : "") +
|
||||
">Exact allow</option>" +
|
||||
'<option value="allow/regex"' +
|
||||
(data.type === "allow" && data.kind === "regex" ? " selected" : "") +
|
||||
">Regex allow</option>" +
|
||||
'<option value="deny/exact"' +
|
||||
(data.type === "deny" && data.kind === "exact" ? " selected " : "") +
|
||||
">Exact deny</option>" +
|
||||
'<option value="deny/regex"' +
|
||||
(data.type === "deny" && data.kind === "regex" ? " selected" : "") +
|
||||
">Regex deny</option>" +
|
||||
"</select>" +
|
||||
"<input type='hidden' id='old_type_" +
|
||||
dataId +
|
||||
"' value='" +
|
||||
data.type +
|
||||
"/" +
|
||||
data.kind +
|
||||
"'>"
|
||||
);
|
||||
var typeEl = $("#type_" + dataId, row);
|
||||
typeEl.on("change", editDomain);
|
||||
|
||||
// Initialize bootstrap-toggle for status field (enabled/disabled)
|
||||
$("td:eq(3)", row).html(
|
||||
'<input type="checkbox" id="enabled_' +
|
||||
dataId +
|
||||
'"' +
|
||||
(data.enabled ? " checked" : "") +
|
||||
">"
|
||||
);
|
||||
var statusEl = $("#enabled_" + dataId, row);
|
||||
statusEl.bootstrapToggle({
|
||||
on: "Enabled",
|
||||
off: "Disabled",
|
||||
size: "small",
|
||||
onstyle: "success",
|
||||
width: "80px",
|
||||
});
|
||||
statusEl.on("change", editDomain);
|
||||
|
||||
// Comment field
|
||||
$("td:eq(4)", row).html('<input id="comment_' + dataId + '" class="form-control">');
|
||||
var commentEl = $("#comment_" + dataId, row);
|
||||
commentEl.val(utils.unescapeHtml(data.comment));
|
||||
commentEl.on("change", editDomain);
|
||||
|
||||
// Group assignment field (multi-select)
|
||||
$("td:eq(5)", row).empty();
|
||||
$("td:eq(5)", row).append(
|
||||
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
|
||||
);
|
||||
var selectEl = $("#multiselect_" + dataId, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var dataSub = "";
|
||||
if (!groups[i].enabled) {
|
||||
dataSub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
|
||||
selectEl.append(
|
||||
$("<option " + dataSub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name)
|
||||
);
|
||||
}
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// Initialize bootstrap-select
|
||||
const applyBtn = "#btn_apply_" + dataId;
|
||||
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");
|
||||
}
|
||||
}
|
||||
})
|
||||
.on("changed.bs.select", function () {
|
||||
// enable Apply button if changes were made to the drop-down menu
|
||||
// and have it call editDomain() on click
|
||||
if ($(applyBtn).prop("disabled")) {
|
||||
$(applyBtn)
|
||||
.addClass("btn-success")
|
||||
.prop("disabled", false)
|
||||
.on("click", function () {
|
||||
editDomain.call(selectEl);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on("hide.bs.select", function () {
|
||||
// Restore values if drop-down menu is closed without clicking the
|
||||
// Apply button (e.g. by clicking outside) and re-disable 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_' +
|
||||
dataId +
|
||||
' class="btn btn-block btn-sm" disabled>Apply</button>'
|
||||
);
|
||||
|
||||
// Highlight row (if url parameter "domainid=" is used)
|
||||
if ("domainid" in GETDict && data.id === parseInt(GETDict.domainid, 10)) {
|
||||
$(row).find("td").addClass("highlight");
|
||||
}
|
||||
|
||||
// Add delete domain button
|
||||
var button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteDomain_' +
|
||||
$('button[id^="deleteDomain_"]').on("click", deleteDomain);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var dataId = utils.hexEncode(data.domain) + "_" + data.type + "_" + data.kind;
|
||||
$(row).attr("data-id", dataId);
|
||||
// Tooltip for domain
|
||||
var tooltip =
|
||||
"Added: " +
|
||||
utils.datetime(data.date_added, false) +
|
||||
"\nLast modified: " +
|
||||
utils.datetime(data.date_modified, false) +
|
||||
"\nDatabase ID: " +
|
||||
data.id;
|
||||
$("td:eq(1)", row).html(
|
||||
'<code id="domain_' +
|
||||
dataId +
|
||||
'" data-id="' +
|
||||
'" title="' +
|
||||
tooltip +
|
||||
'" class="breakall">' +
|
||||
utils.escapeHtml(data.unicode) +
|
||||
(data.domain !== data.unicode ? " (" + utils.escapeHtml(data.domain) + ")" : "") +
|
||||
"</code>"
|
||||
);
|
||||
|
||||
// Drop-down type selector
|
||||
$("td:eq(2)", row).html(
|
||||
'<select id="type_' +
|
||||
dataId +
|
||||
'">' +
|
||||
'<span class="far fa-trash-alt"></span>' +
|
||||
"</button>";
|
||||
$("td:eq(6)", row).html(button);
|
||||
},
|
||||
select: {
|
||||
style: "multi",
|
||||
selector: "td:first-child",
|
||||
info: false,
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "selectNone",
|
||||
text: '<span class="far fa-check-square"></span>',
|
||||
titleAttr: "Deselect All",
|
||||
className: "btn-sm datatable-bt removeAll",
|
||||
},
|
||||
{
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push($(this).attr("data-id"));
|
||||
});
|
||||
// Delete all selected rows at once
|
||||
delItems(ids);
|
||||
},
|
||||
},
|
||||
],
|
||||
dom:
|
||||
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'i>>",
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"],
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
utils.stateSaveCallback("groups-domains-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("groups-domains-table");
|
||||
'" class="form-control">' +
|
||||
'<option value="allow/exact"' +
|
||||
(data.type === "allow" && data.kind === "exact" ? " selected" : "") +
|
||||
">Exact allow</option>" +
|
||||
'<option value="allow/regex"' +
|
||||
(data.type === "allow" && data.kind === "regex" ? " selected" : "") +
|
||||
">Regex allow</option>" +
|
||||
'<option value="deny/exact"' +
|
||||
(data.type === "deny" && data.kind === "exact" ? " selected " : "") +
|
||||
">Exact deny</option>" +
|
||||
'<option value="deny/regex"' +
|
||||
(data.type === "deny" && data.kind === "regex" ? " selected" : "") +
|
||||
">Regex deny</option>" +
|
||||
"</select>" +
|
||||
"<input type='hidden' id='old_type_" +
|
||||
dataId +
|
||||
"' value='" +
|
||||
data.type +
|
||||
"/" +
|
||||
data.kind +
|
||||
"'>"
|
||||
);
|
||||
var typeEl = $("#type_" + dataId, row);
|
||||
typeEl.on("change", editDomain);
|
||||
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
// Initialize bootstrap-toggle for status field (enabled/disabled)
|
||||
$("td:eq(3)", row).html(
|
||||
'<input type="checkbox" id="enabled_' +
|
||||
dataId +
|
||||
'"' +
|
||||
(data.enabled ? " checked" : "") +
|
||||
">"
|
||||
);
|
||||
var statusEl = $("#enabled_" + dataId, row);
|
||||
statusEl.bootstrapToggle({
|
||||
on: "Enabled",
|
||||
off: "Disabled",
|
||||
size: "small",
|
||||
onstyle: "success",
|
||||
width: "80px",
|
||||
});
|
||||
statusEl.on("change", editDomain);
|
||||
|
||||
// Comment field
|
||||
$("td:eq(4)", row).html('<input id="comment_' + dataId + '" class="form-control">');
|
||||
var commentEl = $("#comment_" + dataId, row);
|
||||
commentEl.val(data.comment);
|
||||
commentEl.on("change", editDomain);
|
||||
|
||||
// Group assignment field (multi-select)
|
||||
$("td:eq(5)", row).empty();
|
||||
$("td:eq(5)", row).append(
|
||||
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
|
||||
);
|
||||
var selectEl = $("#multiselect_" + dataId, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var dataSub = "";
|
||||
if (!groups[i].enabled) {
|
||||
dataSub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
|
||||
// Reset visibility of ID column
|
||||
data.columns[0].visible = false;
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
},
|
||||
initComplete: function () {
|
||||
if ("domainid" in GETDict) {
|
||||
var pos = table
|
||||
.column(0, { order: "current" })
|
||||
.data()
|
||||
.indexOf(parseInt(GETDict.domainid, 10));
|
||||
if (pos >= 0) {
|
||||
var page = Math.floor(pos / table.page.info().length);
|
||||
table.page(page).draw(false);
|
||||
selectEl.append(
|
||||
$("<option " + dataSub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name)
|
||||
);
|
||||
}
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// Initialize bootstrap-select
|
||||
const applyBtn = "#btn_apply_" + dataId;
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on("changed.bs.select", function () {
|
||||
// enable Apply button if changes were made to the drop-down menu
|
||||
// and have it call editDomain() on click
|
||||
if ($(applyBtn).prop("disabled")) {
|
||||
$(applyBtn)
|
||||
.addClass("btn-success")
|
||||
.prop("disabled", false)
|
||||
.on("click", function () {
|
||||
editDomain.call(selectEl);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on("hide.bs.select", function () {
|
||||
// Restore values if drop-down menu is closed without clicking the
|
||||
// Apply button (e.g. by clicking outside) and re-disable 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_' +
|
||||
dataId +
|
||||
' class="btn btn-block btn-sm" disabled>Apply</button>'
|
||||
);
|
||||
|
||||
// Highlight row (if url parameter "domainid=" is used)
|
||||
if ("domainid" in GETDict && data.id === parseInt(GETDict.domainid, 10)) {
|
||||
$(row).find("td").addClass("highlight");
|
||||
}
|
||||
|
||||
// Add delete domain button
|
||||
var button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteDomain_' +
|
||||
dataId +
|
||||
'" data-id="' +
|
||||
dataId +
|
||||
'">' +
|
||||
'<span class="far fa-trash-alt"></span>' +
|
||||
"</button>";
|
||||
$("td:eq(6)", row).html(button);
|
||||
},
|
||||
select: {
|
||||
style: "multi",
|
||||
selector: "td:first-child",
|
||||
info: false,
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "selectNone",
|
||||
text: '<span class="far fa-check-square"></span>',
|
||||
titleAttr: "Deselect All",
|
||||
className: "btn-sm datatable-bt removeAll",
|
||||
},
|
||||
{
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push($(this).attr("data-id"));
|
||||
});
|
||||
// Delete all selected rows at once
|
||||
delItems(ids);
|
||||
},
|
||||
},
|
||||
],
|
||||
dom:
|
||||
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'i>>",
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"],
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
utils.stateSaveCallback("groups-domains-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("groups-domains-table");
|
||||
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reset visibility of ID column
|
||||
data.columns[0].visible = false;
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
},
|
||||
initComplete: function () {
|
||||
if ("domainid" in GETDict) {
|
||||
var pos = table
|
||||
.column(0, { order: "current" })
|
||||
.data()
|
||||
.indexOf(parseInt(GETDict.domainid, 10));
|
||||
if (pos >= 0) {
|
||||
var page = Math.floor(pos / table.page.info().length);
|
||||
table.page(page).draw(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
// Disable autocorrect in the search box
|
||||
var input = document.querySelector("input[type=search]");
|
||||
if (input !== null) {
|
||||
@@ -449,7 +448,7 @@ function delItems(ids) {
|
||||
|
||||
utils.disableAll();
|
||||
const idstring = ids.join(", ");
|
||||
utils.showAlert("info", "", "Deleting domain...", "<ul>" + domain + "</ul>");
|
||||
utils.showAlert("info", "", "Deleting domain...", domain);
|
||||
|
||||
$.ajax({
|
||||
url: "/api/domains/" + typestr + "/" + encodeURIComponent(domain),
|
||||
@@ -505,11 +504,11 @@ function addDomain() {
|
||||
commentEl = $("#new_regex_comment");
|
||||
}
|
||||
|
||||
const comment = utils.escapeHtml(commentEl.val());
|
||||
const comment = commentEl.val();
|
||||
|
||||
// Check if the user wants to add multiple domains (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
var domains = utils.escapeHtml(domainEl.val()).split(/[\s,]+/);
|
||||
var domains = domainEl.val().split(/\s+/);
|
||||
// Remove empty elements
|
||||
domains = domains.filter(function (el) {
|
||||
return el !== "";
|
||||
@@ -544,6 +543,7 @@ function addDomain() {
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
domain: domains,
|
||||
comment: comment,
|
||||
@@ -553,6 +553,10 @@ function addDomain() {
|
||||
success: function (data) {
|
||||
utils.enableAll();
|
||||
utils.listsAlert("domain", domains, data);
|
||||
$("#new_domain").val("");
|
||||
$("#new_domain_comment").val("");
|
||||
$("#new_regex").val("");
|
||||
$("#new_regex_comment").val("");
|
||||
table.ajax.reload(null, false);
|
||||
table.rows().deselect();
|
||||
|
||||
@@ -575,7 +579,7 @@ function editDomain() {
|
||||
const newTypestr = tr.find("#type_" + domain).val();
|
||||
const oldTypeStr = tr.find("#old_type_" + domain).val();
|
||||
const enabled = tr.find("#enabled_" + domain).is(":checked");
|
||||
const comment = utils.escapeHtml(tr.find("#comment_" + domain).val());
|
||||
const comment = tr.find("#comment_" + domain).val();
|
||||
// Convert list of string integers to list of integers using map
|
||||
const groups = tr
|
||||
.find("#multiselect_" + domain)
|
||||
@@ -621,12 +625,13 @@ function editDomain() {
|
||||
|
||||
utils.disableAll();
|
||||
const domainDecoded = utils.hexDecode(domain.split("_")[0]);
|
||||
utils.showAlert("info", "", "Editing domain...", domain);
|
||||
utils.showAlert("info", "", "Editing domain...", domainDecoded);
|
||||
$.ajax({
|
||||
url: "/api/domains/" + newTypestr + "/" + encodeURIComponent(domainDecoded),
|
||||
method: "put",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
groups: groups,
|
||||
comment: comment,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, groups:false, apiFailure:false, updateFtlInfo:false, getGroups:false, processGroupResult:false */
|
||||
/* exported initTable */
|
||||
|
||||
var table;
|
||||
var GETDict = {};
|
||||
@@ -17,7 +18,7 @@ $(function () {
|
||||
$("#btnAddBlock").on("click", { type: "block" }, addList);
|
||||
|
||||
utils.setBsSelectDefaults();
|
||||
initTable();
|
||||
getGroups();
|
||||
});
|
||||
|
||||
function format(data) {
|
||||
@@ -98,305 +99,306 @@ function format(data) {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function initTable() {
|
||||
table = $("#listsTable")
|
||||
.on("preXhr.dt", function () {
|
||||
getGroups();
|
||||
})
|
||||
.DataTable({
|
||||
processing: true,
|
||||
ajax: {
|
||||
url: "/api/lists",
|
||||
dataSrc: "lists",
|
||||
type: "GET",
|
||||
},
|
||||
order: [[0, "asc"]],
|
||||
columns: [
|
||||
{ data: "id", visible: false },
|
||||
{ data: null, visible: true, orderable: false, width: "15px" },
|
||||
{ data: "status", searchable: false, class: "details-control" },
|
||||
{ data: "address" },
|
||||
{ data: "enabled", searchable: false },
|
||||
{ data: "comment" },
|
||||
{ data: "groups", searchable: false },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
],
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 1,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
return "";
|
||||
},
|
||||
table = $("#listsTable").DataTable({
|
||||
processing: true,
|
||||
ajax: {
|
||||
url: "/api/lists",
|
||||
dataSrc: "lists",
|
||||
type: "GET",
|
||||
},
|
||||
order: [[0, "asc"]],
|
||||
columns: [
|
||||
{ data: "id", visible: false },
|
||||
{ data: null, visible: true, orderable: false, width: "15px" },
|
||||
{ data: "status", searchable: false, class: "details-control" },
|
||||
{ data: "address" },
|
||||
{ data: "enabled", searchable: false },
|
||||
{ data: "comment" },
|
||||
{ data: "groups", searchable: false },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
],
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 1,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
return "";
|
||||
},
|
||||
{
|
||||
targets: "_all",
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
// Hide buttons if all lists were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
$('button[id^="deleteList_"]').on("click", deleteList);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var dataId = utils.hexEncode(data.address);
|
||||
$(row).attr("data-id", dataId);
|
||||
$(row).attr("data-type", data.type);
|
||||
{
|
||||
targets: "_all",
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
// Hide buttons if all lists were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
var statusCode = 0,
|
||||
statusIcon;
|
||||
// If there is no status or the list is disabled, we keep
|
||||
// status 0 (== unknown)
|
||||
if (data.status !== null && data.enabled) {
|
||||
statusCode = parseInt(data.status, 10);
|
||||
}
|
||||
$('button[id^="deleteList_"]').on("click", deleteList);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var dataId = utils.hexEncode(data.address);
|
||||
$(row).attr("data-id", dataId);
|
||||
$(row).attr("data-type", data.type);
|
||||
|
||||
switch (statusCode) {
|
||||
case 1:
|
||||
statusIcon = "fa-check";
|
||||
break;
|
||||
case 2:
|
||||
statusIcon = "fa-history";
|
||||
break;
|
||||
case 3:
|
||||
statusIcon = "fa-exclamation-circle";
|
||||
break;
|
||||
case 4:
|
||||
statusIcon = "fa-times";
|
||||
break;
|
||||
default:
|
||||
statusIcon = "fa-question-circle";
|
||||
break;
|
||||
}
|
||||
var statusCode = 0,
|
||||
statusIcon;
|
||||
// If there is no status or the list is disabled, we keep
|
||||
// status 0 (== unknown)
|
||||
if (data.status !== null && data.enabled) {
|
||||
statusCode = parseInt(data.status, 10);
|
||||
}
|
||||
|
||||
// Add red minus sign icon if data["type"] is "block"
|
||||
// Add green plus sign icon if data["type"] is "allow"
|
||||
let status =
|
||||
"<i class='fa fa-fw fa-question-circle text-orange' title='This list is of unknown type'></i>";
|
||||
if (data.type === "block") {
|
||||
status = "<i class='fa fa-fw fa-minus text-red' title='This is a blocklist'></i>";
|
||||
} else if (data.type === "allow") {
|
||||
status = "<i class='fa fa-fw fa-plus text-green' title='This is an allowlist'></i>";
|
||||
}
|
||||
switch (statusCode) {
|
||||
case 1:
|
||||
statusIcon = "fa-check";
|
||||
break;
|
||||
case 2:
|
||||
statusIcon = "fa-history";
|
||||
break;
|
||||
case 3:
|
||||
statusIcon = "fa-exclamation-circle";
|
||||
break;
|
||||
case 4:
|
||||
statusIcon = "fa-times";
|
||||
break;
|
||||
default:
|
||||
statusIcon = "fa-question-circle";
|
||||
break;
|
||||
}
|
||||
|
||||
$("td:eq(1)", row).addClass("list-status-" + statusCode);
|
||||
$("td:eq(1)", row).html(
|
||||
"<i class='fa fa-fw " +
|
||||
statusIcon +
|
||||
"' title='Click for details about this list'></i>" +
|
||||
status
|
||||
);
|
||||
// Add red minus sign icon if data["type"] is "block"
|
||||
// Add green plus sign icon if data["type"] is "allow"
|
||||
let status =
|
||||
"<i class='fa fa-fw fa-question-circle text-orange' title='This list is of unknown type'></i>";
|
||||
if (data.type === "block") {
|
||||
status = "<i class='fa fa-fw fa-minus text-red' title='This is a blocklist'></i>";
|
||||
} else if (data.type === "allow") {
|
||||
status = "<i class='fa fa-fw fa-plus text-green' title='This is an allowlist'></i>";
|
||||
}
|
||||
|
||||
if (data.address.startsWith("file://")) {
|
||||
// Local files cannot be downloaded from a distant client so don't show
|
||||
// a link to such a list here
|
||||
$("td:eq(2)", row).html(
|
||||
'<code id="address_' + dataId + '" class="breakall">' + data.address + "</code>"
|
||||
);
|
||||
} else {
|
||||
$("td:eq(2)", row).html(
|
||||
'<a id="address_' +
|
||||
dataId +
|
||||
'" class="breakall" href="' +
|
||||
data.address +
|
||||
'" target="_blank" rel="noopener noreferrer">' +
|
||||
data.address +
|
||||
"</a>"
|
||||
);
|
||||
}
|
||||
$("td:eq(1)", row).addClass("list-status-" + statusCode);
|
||||
$("td:eq(1)", row).html(
|
||||
"<i class='fa fa-fw " +
|
||||
statusIcon +
|
||||
"' title='Click for details about this list'></i>" +
|
||||
status
|
||||
);
|
||||
|
||||
$("td:eq(3)", row).html(
|
||||
'<input type="checkbox" id="enabled_' +
|
||||
if (data.address.startsWith("file://")) {
|
||||
// Local files cannot be downloaded from a distant client so don't show
|
||||
// a link to such a list here
|
||||
$("td:eq(2)", row).html(
|
||||
'<code id="address_' +
|
||||
dataId +
|
||||
'"' +
|
||||
(data.enabled ? " checked" : "") +
|
||||
">"
|
||||
'" class="breakall">' +
|
||||
utils.escapeHtml(data.address) +
|
||||
"</code>"
|
||||
);
|
||||
var statusEl = $("#enabled_" + dataId, row);
|
||||
statusEl.bootstrapToggle({
|
||||
on: "Enabled",
|
||||
off: "Disabled",
|
||||
size: "small",
|
||||
onstyle: "success",
|
||||
width: "80px",
|
||||
});
|
||||
statusEl.on("change", editList);
|
||||
|
||||
$("td:eq(4)", row).html('<input id="comment_' + dataId + '" class="form-control">');
|
||||
var commentEl = $("#comment_" + dataId, row);
|
||||
commentEl.val(utils.unescapeHtml(data.comment));
|
||||
commentEl.on("change", editList);
|
||||
|
||||
$("td:eq(5)", row).empty();
|
||||
$("td:eq(5)", row).append(
|
||||
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
|
||||
} else {
|
||||
$("td:eq(2)", row).html(
|
||||
'<a id="address_' +
|
||||
dataId +
|
||||
'" class="breakall" href="' +
|
||||
encodeURI(data.address) +
|
||||
'" target="_blank" rel="noopener noreferrer">' +
|
||||
utils.escapeHtml(data.address) +
|
||||
"</a>"
|
||||
);
|
||||
var selectEl = $("#multiselect_" + dataId, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var dataSub = "";
|
||||
if (!groups[i].enabled) {
|
||||
dataSub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
}
|
||||
|
||||
selectEl.append(
|
||||
$("<option " + dataSub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name)
|
||||
);
|
||||
}
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
})
|
||||
.on("changed.bs.select", function () {
|
||||
// enable Apply button
|
||||
if ($(applyBtn).prop("disabled")) {
|
||||
$(applyBtn)
|
||||
.addClass("btn-success")
|
||||
.prop("disabled", false)
|
||||
.on("click", function () {
|
||||
editList.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_' +
|
||||
dataId +
|
||||
' class="btn btn-block btn-sm" disabled>Apply</button>'
|
||||
);
|
||||
|
||||
var applyBtn = "#btn_apply_" + dataId;
|
||||
|
||||
// Highlight row (if url parameter "listid=" is used)
|
||||
if ("listid" in GETDict && data.id === parseInt(GETDict.listid, 10)) {
|
||||
$(row).find("td").addClass("highlight");
|
||||
}
|
||||
|
||||
var button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteList_' +
|
||||
$("td:eq(3)", row).html(
|
||||
'<input type="checkbox" id="enabled_' +
|
||||
dataId +
|
||||
'" data-id="' +
|
||||
dataId +
|
||||
'">' +
|
||||
'<span class="far fa-trash-alt"></span>' +
|
||||
"</button>";
|
||||
$("td:eq(6)", row).html(button);
|
||||
},
|
||||
dom:
|
||||
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'i>>",
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"],
|
||||
],
|
||||
select: {
|
||||
style: "multi",
|
||||
selector: "td:not(:last-child)",
|
||||
info: false,
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "selectNone",
|
||||
text: '<span class="far fa-check-square"></span>',
|
||||
titleAttr: "Deselect All",
|
||||
className: "btn-sm datatable-bt removeAll",
|
||||
},
|
||||
{
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push($(this).attr("data-id"));
|
||||
});
|
||||
// Delete all selected rows at once
|
||||
delItems(ids);
|
||||
},
|
||||
},
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
utils.stateSaveCallback("groups-lists-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("groups-lists-table");
|
||||
'"' +
|
||||
(data.enabled ? " checked" : "") +
|
||||
">"
|
||||
);
|
||||
var statusEl = $("#enabled_" + dataId, row);
|
||||
statusEl.bootstrapToggle({
|
||||
on: "Enabled",
|
||||
off: "Disabled",
|
||||
size: "small",
|
||||
onstyle: "success",
|
||||
width: "80px",
|
||||
});
|
||||
statusEl.on("change", editList);
|
||||
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
$("td:eq(4)", row).html('<input id="comment_' + dataId + '" class="form-control">');
|
||||
var commentEl = $("#comment_" + dataId, row);
|
||||
commentEl.val(data.comment);
|
||||
commentEl.on("change", editList);
|
||||
|
||||
$("td:eq(5)", row).empty();
|
||||
$("td:eq(5)", row).append(
|
||||
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
|
||||
);
|
||||
var selectEl = $("#multiselect_" + dataId, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var dataSub = "";
|
||||
if (!groups[i].enabled) {
|
||||
dataSub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
|
||||
// Reset visibility of ID column
|
||||
data.columns[0].visible = false;
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
},
|
||||
initComplete: function () {
|
||||
if ("listid" in GETDict) {
|
||||
var pos = table
|
||||
.column(0, { order: "current" })
|
||||
.data()
|
||||
.indexOf(parseInt(GETDict.listid, 10));
|
||||
if (pos >= 0) {
|
||||
var page = Math.floor(pos / table.page.info().length);
|
||||
table.page(page).draw(false);
|
||||
selectEl.append(
|
||||
$("<option " + dataSub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name)
|
||||
);
|
||||
}
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on("changed.bs.select", function () {
|
||||
// enable Apply button
|
||||
if ($(applyBtn).prop("disabled")) {
|
||||
$(applyBtn)
|
||||
.addClass("btn-success")
|
||||
.prop("disabled", false)
|
||||
.on("click", function () {
|
||||
editList.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_' +
|
||||
dataId +
|
||||
' class="btn btn-block btn-sm" disabled>Apply</button>'
|
||||
);
|
||||
|
||||
var applyBtn = "#btn_apply_" + dataId;
|
||||
|
||||
// Highlight row (if url parameter "listid=" is used)
|
||||
if ("listid" in GETDict && data.id === parseInt(GETDict.listid, 10)) {
|
||||
$(row).find("td").addClass("highlight");
|
||||
}
|
||||
|
||||
var button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteList_' +
|
||||
dataId +
|
||||
'" data-id="' +
|
||||
dataId +
|
||||
'">' +
|
||||
'<span class="far fa-trash-alt"></span>' +
|
||||
"</button>";
|
||||
$("td:eq(6)", row).html(button);
|
||||
},
|
||||
dom:
|
||||
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
|
||||
"<'row'<'col-sm-12'i>>",
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"],
|
||||
],
|
||||
select: {
|
||||
style: "multi",
|
||||
selector: "td:not(:last-child)",
|
||||
info: false,
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "selectNone",
|
||||
text: '<span class="far fa-check-square"></span>',
|
||||
titleAttr: "Deselect All",
|
||||
className: "btn-sm datatable-bt removeAll",
|
||||
},
|
||||
{
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push($(this).attr("data-id"));
|
||||
});
|
||||
// Delete all selected rows at once
|
||||
delItems(ids);
|
||||
},
|
||||
},
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
utils.stateSaveCallback("groups-lists-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("groups-lists-table");
|
||||
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reset visibility of ID column
|
||||
data.columns[0].visible = false;
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
},
|
||||
initComplete: function () {
|
||||
if ("listid" in GETDict) {
|
||||
var pos = table
|
||||
.column(0, { order: "current" })
|
||||
.data()
|
||||
.indexOf(parseInt(GETDict.listid, 10));
|
||||
if (pos >= 0) {
|
||||
var page = Math.floor(pos / table.page.info().length);
|
||||
table.page(page).draw(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
table.on("init select deselect", function () {
|
||||
utils.changeBulkDeleteStates(table);
|
||||
@@ -464,7 +466,7 @@ function delItems(ids) {
|
||||
|
||||
utils.disableAll();
|
||||
const idstring = ids.join(", ");
|
||||
utils.showAlert("info", "", "Deleting list(s) ...", "<ul>" + address + "</ul>");
|
||||
utils.showAlert("info", "", "Deleting list(s) ...", address);
|
||||
|
||||
$.ajax({
|
||||
url: "/api/lists/" + encodeURIComponent(address),
|
||||
@@ -499,11 +501,13 @@ function delItems(ids) {
|
||||
|
||||
function addList(event) {
|
||||
const type = event.data.type;
|
||||
const comment = utils.escapeHtml($("#new_comment").val());
|
||||
const comment = $("#new_comment").val();
|
||||
|
||||
// Check if the user wants to add multiple domains (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
var addresses = utils.escapeHtml($("#new_address").val()).split(/[\s,]+/);
|
||||
var addresses = $("#new_address")
|
||||
.val()
|
||||
.split(/[\s,]+/);
|
||||
// Remove empty elements
|
||||
addresses = addresses.filter(function (el) {
|
||||
return el !== "";
|
||||
@@ -525,10 +529,13 @@ function addList(event) {
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ address: addresses, comment: comment, type: type }),
|
||||
success: function (data) {
|
||||
utils.enableAll();
|
||||
utils.listsAlert("list", addresses, data);
|
||||
$("#new_address").val("");
|
||||
$("#new_comment").val("");
|
||||
table.ajax.reload(null, false);
|
||||
table.rows().deselect();
|
||||
|
||||
@@ -591,6 +598,7 @@ function editList() {
|
||||
method: "put",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
groups: groups,
|
||||
comment: comment,
|
||||
|
||||
@@ -75,7 +75,7 @@ $(function () {
|
||||
'<input id="name_' + data.id + '" title="' + tooltip + '" class="form-control">'
|
||||
);
|
||||
var nameEl = $("#name_" + data.id, row);
|
||||
nameEl.val(utils.unescapeHtml(data.name));
|
||||
nameEl.val(data.name);
|
||||
nameEl.on("change", editGroup);
|
||||
|
||||
$("td:eq(2)", row).html(
|
||||
@@ -98,7 +98,7 @@ $(function () {
|
||||
$("td:eq(3)", row).html('<input id="comment_' + data.id + '" class="form-control">');
|
||||
var comment = data.comment !== null ? data.comment : "";
|
||||
var commentEl = $("#comment_" + data.id, row);
|
||||
commentEl.val(utils.unescapeHtml(comment));
|
||||
commentEl.val(comment);
|
||||
commentEl.on("change", editGroup);
|
||||
|
||||
$("td:eq(4)", row).empty();
|
||||
@@ -277,7 +277,7 @@ function delItems(ids) {
|
||||
}
|
||||
|
||||
function addGroup() {
|
||||
const comment = utils.escapeHtml($("#new_comment").val());
|
||||
const comment = $("#new_comment").val();
|
||||
|
||||
// Check if the user wants to add multiple groups (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
@@ -306,6 +306,7 @@ function addGroup() {
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
name: names,
|
||||
comment: comment,
|
||||
@@ -336,9 +337,9 @@ function editGroup() {
|
||||
const tr = $(this).closest("tr");
|
||||
const id = tr.attr("data-id");
|
||||
const oldName = idNames[id];
|
||||
const name = utils.escapeHtml(tr.find("#name_" + id).val());
|
||||
const name = tr.find("#name_" + id).val();
|
||||
const enabled = tr.find("#enabled_" + id).is(":checked");
|
||||
const comment = utils.escapeHtml(tr.find("#comment_" + id).val());
|
||||
const comment = tr.find("#comment_" + id).val();
|
||||
|
||||
var done = "edited";
|
||||
var notDone = "editing";
|
||||
@@ -373,6 +374,7 @@ function editGroup() {
|
||||
method: "put",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
name: name,
|
||||
comment: comment,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, Chart:false, apiFailure:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false */
|
||||
/* global utils:false, Chart:false, apiFailure:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false, REFRESH_INTERVAL: false */
|
||||
|
||||
// Define global variables
|
||||
var timeLineChart, clientsChart;
|
||||
@@ -71,16 +71,14 @@ function updateQueriesOverTime() {
|
||||
timeLineChart.update();
|
||||
})
|
||||
.done(function () {
|
||||
// Reload graph after 10 minutes
|
||||
failures = 0;
|
||||
setTimeout(updateQueriesOverTime, 600000);
|
||||
utils.setTimer(updateQueriesOverTime, REFRESH_INTERVAL.history);
|
||||
})
|
||||
.fail(function () {
|
||||
failures++;
|
||||
if (failures < 5) {
|
||||
// Try again after 1 minute only if this has not failed more
|
||||
// than five times in a row
|
||||
setTimeout(updateQueriesOverTime, 60000);
|
||||
// Try again ´only if this has not failed more than five times in a row
|
||||
utils.setTimer(updateQueriesOverTime, 0.1 * REFRESH_INTERVAL.history);
|
||||
}
|
||||
})
|
||||
.fail(function (data) {
|
||||
@@ -123,8 +121,7 @@ function updateQueryTypesPie() {
|
||||
queryTypePieChart.update("none");
|
||||
})
|
||||
.done(function () {
|
||||
// Reload graph after minute
|
||||
setTimeout(updateQueryTypesPie, 60000);
|
||||
utils.setTimer(updateQueryTypesPie, REFRESH_INTERVAL.query_types);
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
@@ -185,14 +182,13 @@ function updateClientsOverTime() {
|
||||
.done(function () {
|
||||
// Reload graph after 10 minutes
|
||||
failures = 0;
|
||||
setTimeout(updateClientsOverTime, 600000);
|
||||
utils.setTimer(updateClientsOverTime, REFRESH_INTERVAL.clients);
|
||||
})
|
||||
.fail(function () {
|
||||
failures++;
|
||||
if (failures < 5) {
|
||||
// Try again after 1 minute only if this has not failed more
|
||||
// than five times in a row
|
||||
setTimeout(updateClientsOverTime, 60000);
|
||||
// Try again only if this has not failed more than five times in a row
|
||||
utils.setTimer(updateClientsOverTime, 0.1 * REFRESH_INTERVAL.clients);
|
||||
}
|
||||
})
|
||||
.fail(function (data) {
|
||||
@@ -200,6 +196,7 @@ function updateClientsOverTime() {
|
||||
});
|
||||
}
|
||||
|
||||
var upstreams = {};
|
||||
function updateForwardDestinationsPie() {
|
||||
$.getJSON("/api/stats/upstreams", function (data) {
|
||||
var v = [],
|
||||
@@ -221,6 +218,12 @@ function updateForwardDestinationsPie() {
|
||||
label += "#" + item.port;
|
||||
}
|
||||
|
||||
// Store upstreams for generating links to the Query Log
|
||||
upstreams[label] = item.ip;
|
||||
if (item.port > 0) {
|
||||
upstreams[label] += "#" + item.port;
|
||||
}
|
||||
|
||||
var percent = (100 * item.count) / sum;
|
||||
values.push([label, percent, THEME_COLORS[i++ % THEME_COLORS.length]]);
|
||||
});
|
||||
@@ -246,8 +249,7 @@ function updateForwardDestinationsPie() {
|
||||
forwardDestinationPieChart.update("none");
|
||||
})
|
||||
.done(function () {
|
||||
// Reload graph after one minute
|
||||
setTimeout(updateForwardDestinationsPie, 60000);
|
||||
utils.setTimer(updateForwardDestinationsPie, REFRESH_INTERVAL.upstreams);
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
@@ -255,7 +257,7 @@ function updateForwardDestinationsPie() {
|
||||
}
|
||||
|
||||
function updateTopClientsTable(blocked) {
|
||||
var api, style, tablecontent, overlay, clienttable;
|
||||
let api, style, tablecontent, overlay, clienttable;
|
||||
if (blocked) {
|
||||
api = "/api/stats/top_clients?blocked=true";
|
||||
style = "queries-blocked";
|
||||
@@ -273,9 +275,8 @@ function updateTopClientsTable(blocked) {
|
||||
$.getJSON(api, function (data) {
|
||||
// Clear tables before filling them with data
|
||||
tablecontent.remove();
|
||||
var url,
|
||||
percentage,
|
||||
sum = blocked ? data.blocked_queries : data.total_queries;
|
||||
let url, percentage;
|
||||
const sum = blocked ? data.blocked_queries : data.total_queries;
|
||||
|
||||
// Add note if there are no results (e.g. privacy mode enabled)
|
||||
if (jQuery.isEmptyObject(data.clients)) {
|
||||
@@ -285,10 +286,14 @@ function updateTopClientsTable(blocked) {
|
||||
// Populate table with content
|
||||
data.clients.forEach(function (client) {
|
||||
// Sanitize client
|
||||
var clientname = utils.escapeHtml(client.name);
|
||||
var clientip = utils.escapeHtml(client.ip);
|
||||
if (clientname.length === 0) clientname = clientip;
|
||||
url = '<a href="queries.lp?client_ip=' + clientip + '">' + clientname + "</a>";
|
||||
let clientname = client.name;
|
||||
if (clientname.length === 0) clientname = client.ip;
|
||||
url =
|
||||
'<a href="queries.lp?client_ip=' +
|
||||
encodeURIComponent(client.ip) +
|
||||
'">' +
|
||||
utils.escapeHtml(clientname) +
|
||||
"</a>";
|
||||
percentage = (client.count / sum) * 100;
|
||||
|
||||
// Add row to table
|
||||
@@ -309,7 +314,7 @@ function updateTopClientsTable(blocked) {
|
||||
}
|
||||
|
||||
function updateTopDomainsTable(blocked) {
|
||||
var api, style, tablecontent, overlay, domaintable;
|
||||
let api, style, tablecontent, overlay, domaintable;
|
||||
if (blocked) {
|
||||
api = "/api/stats/top_domains?blocked=true";
|
||||
style = "queries-blocked";
|
||||
@@ -327,11 +332,8 @@ function updateTopDomainsTable(blocked) {
|
||||
$.getJSON(api, function (data) {
|
||||
// Clear tables before filling them with data
|
||||
tablecontent.remove();
|
||||
var url,
|
||||
domain,
|
||||
percentage,
|
||||
urlText,
|
||||
sum = blocked ? data.blocked_queries : data.total_queries;
|
||||
let url, domain, percentage, urlText;
|
||||
const sum = blocked ? data.blocked_queries : data.total_queries;
|
||||
|
||||
// Add note if there are no results (e.g. privacy mode enabled)
|
||||
if (jQuery.isEmptyObject(data.domains)) {
|
||||
@@ -341,7 +343,7 @@ function updateTopDomainsTable(blocked) {
|
||||
// Populate table with content
|
||||
data.domains.forEach(function (item) {
|
||||
// Sanitize domain
|
||||
domain = utils.escapeHtml(item.domain);
|
||||
domain = encodeURIComponent(item.domain);
|
||||
// Substitute "." for empty domain lookups
|
||||
urlText = domain === "" ? "." : domain;
|
||||
url = '<a href="queries.lp?domain=' + domain + '">' + urlText + "</a>";
|
||||
@@ -375,7 +377,7 @@ function updateTopLists() {
|
||||
updateTopClientsTable(false);
|
||||
|
||||
// Update top lists data every 10 seconds
|
||||
setTimeout(updateTopLists, 10000);
|
||||
utils.setTimer(updateTopLists, REFRESH_INTERVAL.top_lists);
|
||||
}
|
||||
|
||||
function glowIfChanged(elem, textData) {
|
||||
@@ -385,13 +387,7 @@ function glowIfChanged(elem, textData) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSummaryData(runOnce) {
|
||||
var setTimer = function (timeInSeconds) {
|
||||
if (!runOnce) {
|
||||
setTimeout(updateSummaryData, timeInSeconds * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
function updateSummaryData(runOnce = false) {
|
||||
$.getJSON("/api/stats/summary", function (data) {
|
||||
var intl = new Intl.NumberFormat();
|
||||
glowIfChanged($("span#dns_queries"), intl.format(parseInt(data.queries.total, 10)));
|
||||
@@ -415,10 +411,10 @@ function updateSummaryData(runOnce) {
|
||||
}, 500);
|
||||
})
|
||||
.done(function () {
|
||||
setTimer(1);
|
||||
if (!runOnce) utils.setTimer(updateSummaryData, REFRESH_INTERVAL.summary);
|
||||
})
|
||||
.fail(function (data) {
|
||||
setTimer(300);
|
||||
utils.setTimer(updateSummaryData, 3 * REFRESH_INTERVAL.summary);
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ function doLogin(password) {
|
||||
method: "POST",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ password: password, totp: parseInt($("#totp").val(), 10) }),
|
||||
})
|
||||
.done(function (data) {
|
||||
|
||||
@@ -203,7 +203,7 @@ $(function () {
|
||||
order: [[6, "desc"]],
|
||||
columns: [
|
||||
{ data: "id", visible: false },
|
||||
{ data: "ip", type: "ip-address", width: "25%" },
|
||||
{ data: "ips[].ip", type: "ip-address", width: "25%" },
|
||||
{ data: "hwaddr", width: "10%" },
|
||||
{ data: "interface", width: "4%" },
|
||||
{
|
||||
|
||||
@@ -85,6 +85,7 @@ function parseQueryStatus(data) {
|
||||
buttontext,
|
||||
icon = null,
|
||||
colorClass = false,
|
||||
blocked = false,
|
||||
isCNAME = false;
|
||||
switch (data.status) {
|
||||
case "GRAVITY":
|
||||
@@ -93,6 +94,7 @@ function parseQueryStatus(data) {
|
||||
fieldtext = "Blocked (gravity)";
|
||||
buttontext =
|
||||
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
|
||||
blocked = true;
|
||||
break;
|
||||
case "FORWARDED":
|
||||
colorClass = "text-green";
|
||||
@@ -114,6 +116,7 @@ function parseQueryStatus(data) {
|
||||
fieldtext = "Blocked (regex)";
|
||||
buttontext =
|
||||
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
|
||||
blocked = true;
|
||||
break;
|
||||
case "DENYLIST":
|
||||
colorClass = "text-red";
|
||||
@@ -121,24 +124,28 @@ function parseQueryStatus(data) {
|
||||
fieldtext = "Blocked (exact)";
|
||||
buttontext =
|
||||
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
|
||||
blocked = true;
|
||||
break;
|
||||
case "EXTERNAL_BLOCKED_IP":
|
||||
colorClass = "text-red";
|
||||
icon = "fa-solid fa-ban";
|
||||
fieldtext = "Blocked (external, IP)";
|
||||
buttontext = "";
|
||||
blocked = true;
|
||||
break;
|
||||
case "EXTERNAL_BLOCKED_NULL":
|
||||
colorClass = "text-red";
|
||||
icon = "fa-solid fa-ban";
|
||||
fieldtext = "Blocked (external, NULL)";
|
||||
buttontext = "";
|
||||
blocked = true;
|
||||
break;
|
||||
case "EXTERNAL_BLOCKED_NXRA":
|
||||
colorClass = "text-red";
|
||||
icon = "fa-solid fa-ban";
|
||||
fieldtext = "Blocked (external, NXRA)";
|
||||
buttontext = "";
|
||||
blocked = true;
|
||||
break;
|
||||
case "GRAVITY_CNAME":
|
||||
colorClass = "text-red";
|
||||
@@ -147,6 +154,7 @@ function parseQueryStatus(data) {
|
||||
buttontext =
|
||||
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
|
||||
isCNAME = true;
|
||||
blocked = true;
|
||||
break;
|
||||
case "REGEX_CNAME":
|
||||
colorClass = "text-red";
|
||||
@@ -155,6 +163,7 @@ function parseQueryStatus(data) {
|
||||
buttontext =
|
||||
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
|
||||
isCNAME = true;
|
||||
blocked = true;
|
||||
break;
|
||||
case "DENYLIST_CNAME":
|
||||
colorClass = "text-red";
|
||||
@@ -163,6 +172,7 @@ function parseQueryStatus(data) {
|
||||
buttontext =
|
||||
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
|
||||
isCNAME = true;
|
||||
blocked = true;
|
||||
break;
|
||||
case "RETRIED":
|
||||
colorClass = "text-green";
|
||||
@@ -207,6 +217,7 @@ function parseQueryStatus(data) {
|
||||
icon: icon,
|
||||
isCNAME: isCNAME,
|
||||
matchText: matchText,
|
||||
blocked: blocked,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,8 +230,8 @@ function formatReplyTime(replyTime, type) {
|
||||
return replyTime < 1e-4
|
||||
? (1e6 * replyTime).toFixed(1) + " µs"
|
||||
: replyTime < 1
|
||||
? (1e3 * replyTime).toFixed(1) + " ms"
|
||||
: replyTime.toFixed(1) + " s";
|
||||
? (1e3 * replyTime).toFixed(1) + " ms"
|
||||
: replyTime.toFixed(1) + " s";
|
||||
}
|
||||
|
||||
// else: return the number itself (for sorting and searching)
|
||||
@@ -562,6 +573,9 @@ $(function () {
|
||||
$(row).addClass(querystatus.colorClass);
|
||||
}
|
||||
|
||||
// Define row background color
|
||||
$(row).addClass(querystatus.blocked === true ? "blocked-row" : "allowed-row");
|
||||
|
||||
// Substitute domain by "." if empty
|
||||
var domain = data.domain === 0 ? "." : data.domain;
|
||||
|
||||
@@ -603,8 +617,8 @@ $(function () {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
// Add event listener for opening and closing details
|
||||
$("#all-queries tbody").on("click", "tr", function () {
|
||||
// Add event listener for opening and closing details, except on rows with "details-row" class
|
||||
$("#all-queries tbody").on("click", "tr:not(.details-row)", function () {
|
||||
var tr = $(this);
|
||||
var row = table.row(tr);
|
||||
|
||||
@@ -618,8 +632,8 @@ $(function () {
|
||||
row.child.hide();
|
||||
tr.removeClass("shown");
|
||||
} else {
|
||||
// Open this row
|
||||
row.child(formatInfo(row.data())).show();
|
||||
// Open this row. Add a class to the row
|
||||
row.child(formatInfo(row.data()), "details-row").show();
|
||||
tr.addClass("shown");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -59,11 +59,11 @@ function generateRow(topic, key, value) {
|
||||
"</div>" +
|
||||
'<div class="box-body">' +
|
||||
'<div class="form-group">';
|
||||
var defaultValueHint = "";
|
||||
let defaultValueHint = "";
|
||||
if (value.modified) {
|
||||
defaultValueHint = "";
|
||||
if (value.default !== null) {
|
||||
var defVal = utils.escapeHtml(JSON.stringify(value.default));
|
||||
let defVal = utils.escapeHtml(JSON.stringify(value.default));
|
||||
switch (defVal) {
|
||||
case "true": {
|
||||
defVal = "enabled";
|
||||
|
||||
@@ -88,11 +88,8 @@ $(function () {
|
||||
$("td:eq(9)", row).html(button);
|
||||
if (data.current_session) {
|
||||
ownSessionID = data.id;
|
||||
$("td:eq(7)", row).html(
|
||||
'<strong title="This is the session you are currently using for the web interface">' +
|
||||
data.remote_addr +
|
||||
"</strong>"
|
||||
);
|
||||
$(row).addClass("text-bold");
|
||||
$(row).attr("title", "This is the session you are currently using for the web interface");
|
||||
}
|
||||
|
||||
let icon = "";
|
||||
@@ -191,7 +188,7 @@ $(function () {
|
||||
function deleteThisSession() {
|
||||
// This function is called when a red trash button is clicked
|
||||
// We get the ID of the current item from the data-del-id attribute
|
||||
const thisID = parseInt(this.attr("data-del-id"), 10);
|
||||
const thisID = parseInt($(this).attr("data-del-id"), 10);
|
||||
deleted = 0;
|
||||
deleteOneSession(thisID, 1, false);
|
||||
}
|
||||
@@ -346,7 +343,7 @@ function setAppPassword() {
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
data: JSON.stringify({ config: { webserver: { api: { app_pwhash: apppwhash } } } }),
|
||||
contentType: "application/json",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
})
|
||||
.done(function () {
|
||||
$("#modal-apppw").modal("hide");
|
||||
@@ -399,7 +396,7 @@ function setTOTPSecret(secret) {
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
data: JSON.stringify({ config: { webserver: { api: { totp_secret: secret } } } }),
|
||||
contentType: "application/json",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
})
|
||||
.done(function () {
|
||||
$("#button-enable-totp").addClass("hidden");
|
||||
|
||||
@@ -49,7 +49,7 @@ function populateDataTable(endpoint) {
|
||||
columns = [
|
||||
{ data: null, render: CNAMEdomain },
|
||||
{ data: null, render: CNAMEtarget },
|
||||
{ data: null, render: CNAMEttl },
|
||||
{ data: null, width: "40px", render: CNAMEttl },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
];
|
||||
}
|
||||
@@ -73,6 +73,7 @@ function populateDataTable(endpoint) {
|
||||
type: "GET",
|
||||
dataSrc: `config.dns.${endpoint}`,
|
||||
},
|
||||
autoWidth: false,
|
||||
columns: columns,
|
||||
columnDefs: [
|
||||
{
|
||||
@@ -99,8 +100,10 @@ function populateDataTable(endpoint) {
|
||||
$(`td:eq(${endpoint === "hosts" ? 2 : 3})`, row).html(button);
|
||||
},
|
||||
dom:
|
||||
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
||||
"<'row'<'col-sm-5'l><'col-sm-7'f>>" +
|
||||
"<'row'<'col-sm-12'p>>" +
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-12'p>>" +
|
||||
"<'row'<'col-sm-12'i>>",
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
@@ -173,7 +176,7 @@ function delHosts(elem) {
|
||||
utils.showAlert(
|
||||
"error",
|
||||
"",
|
||||
"Error while deleting DNS record: <code>" + utils.escapeHtml(elem) + "</code>",
|
||||
"Error while deleting DNS record: <code>" + elem + "</code>",
|
||||
data.responseText
|
||||
);
|
||||
console.log(exception); // eslint-disable-line no-console
|
||||
@@ -205,7 +208,7 @@ function delCNAME(elem) {
|
||||
utils.showAlert(
|
||||
"error",
|
||||
"",
|
||||
"Error while deleting CNAME record: <code>" + utils.escapeHtml(elem) + "</code>",
|
||||
"Error while deleting CNAME record: <code>" + elem + "</code>",
|
||||
data.responseText
|
||||
);
|
||||
console.log(exception); // eslint-disable-line no-console
|
||||
@@ -225,6 +228,8 @@ $(document).ready(function () {
|
||||
.done(function () {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added DNS record", elem);
|
||||
$("#Hdomain").val("");
|
||||
$("#Hip").val("");
|
||||
$("#hosts-Table").DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
@@ -249,6 +254,8 @@ $(document).ready(function () {
|
||||
.done(function () {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added CNAME record", elem);
|
||||
$("#Cdomain").val("");
|
||||
$("#Ctarget").val("");
|
||||
$("#cnameRecords-Table").DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
@@ -258,4 +265,7 @@ $(document).ready(function () {
|
||||
console.log(exception); // eslint-disable-line no-console
|
||||
});
|
||||
});
|
||||
|
||||
// Add a small legend below the CNAME table
|
||||
$("#cnameRecords-Table").after("<small>* <b>TTL</b> in seconds <i>(optional)</i></small>");
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global apiFailure:false, Chart:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false */
|
||||
/* global apiFailure:false, Chart:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false, REFRESH_INTERVAL: false, utils: false */
|
||||
|
||||
var hostinfoTimer = null;
|
||||
var cachePieChart = null;
|
||||
@@ -105,9 +105,8 @@ function updateHostInfo() {
|
||||
" " +
|
||||
uname.machine
|
||||
);
|
||||
// Update every 120 seconds
|
||||
clearTimeout(hostinfoTimer);
|
||||
hostinfoTimer = setTimeout(updateHostInfo, 120000);
|
||||
hostinfoTimer = utils.setTimer(updateHostInfo, REFRESH_INTERVAL.hosts);
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
@@ -173,9 +172,8 @@ function updateMetrics() {
|
||||
);
|
||||
|
||||
$("div[id^='sysinfo-metrics-overlay']").hide();
|
||||
// Update every 10 seconds
|
||||
clearTimeout(metricsTimer);
|
||||
metricsTimer = setTimeout(updateMetrics, 10000);
|
||||
metricsTimer = utils.setTimer(updateMetrics, REFRESH_INTERVAL.metrics);
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
@@ -298,6 +296,7 @@ $("#loggingButton").confirm({
|
||||
type: "PATCH",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
})
|
||||
.done(function (data) {
|
||||
|
||||
@@ -114,6 +114,7 @@ function setConfigValues(topic, key, value) {
|
||||
|
||||
function saveSettings() {
|
||||
var settings = {};
|
||||
utils.disableAll();
|
||||
$("[data-key]").each(function () {
|
||||
var key = $(this).data("key");
|
||||
var value = $(this).val();
|
||||
@@ -168,6 +169,7 @@ function saveSettings() {
|
||||
contentType: "application/json; charset=utf-8",
|
||||
})
|
||||
.done(function () {
|
||||
utils.enableAll();
|
||||
// Success
|
||||
utils.showAlert(
|
||||
"success",
|
||||
@@ -178,7 +180,10 @@ function saveSettings() {
|
||||
// Show loading overlay
|
||||
utils.loadingOverlay(true);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(function (data, exception) {
|
||||
utils.enableAll();
|
||||
utils.showAlert("error", "", "Error while applying settings", data.responseText);
|
||||
console.log(exception); // eslint-disable-line no-console
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global moment: false, apiFailure: false, utils: false */
|
||||
/* global moment: false, apiFailure: false, utils: false, REFRESH_INTERVAL: false */
|
||||
|
||||
var nextID = 0;
|
||||
|
||||
// Check every 0.5s for fresh data
|
||||
const interval = 500;
|
||||
var lastPID = -1;
|
||||
|
||||
// Maximum number of lines to display
|
||||
const maxlines = 5000;
|
||||
@@ -25,7 +23,7 @@ const markUpdates = true;
|
||||
function getData() {
|
||||
// Only update when spinner is spinning
|
||||
if (!$("#feed-icon").hasClass("fa-play")) {
|
||||
window.setTimeout(getData, interval);
|
||||
utils.setTimer(getData, REFRESH_INTERVAL.logs);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,12 +39,28 @@ function getData() {
|
||||
method: "GET",
|
||||
})
|
||||
.done(function (data) {
|
||||
// Check if we have a new PID -> FTL was restarted
|
||||
if (lastPID !== data.pid) {
|
||||
if (lastPID !== -1) {
|
||||
$("#output").append("<i class='text-danger'>*** FTL restarted ***</i><br>");
|
||||
}
|
||||
|
||||
// Remember PID
|
||||
lastPID = data.pid;
|
||||
// Reset nextID
|
||||
nextID = 0;
|
||||
|
||||
getData();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set placeholder text if log file is empty and we have no new lines
|
||||
if (data.log.length === 0) {
|
||||
if (nextID === 0) {
|
||||
$("#output").html("<i>*** Log file is empty ***</i>");
|
||||
}
|
||||
|
||||
window.setTimeout(getData, interval);
|
||||
utils.setTimer(getData, REFRESH_INTERVAL.logs);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,11 +104,11 @@ function getData() {
|
||||
// Set filename
|
||||
$("#filename").text(data.file);
|
||||
|
||||
window.setTimeout(getData, interval);
|
||||
utils.setTimer(getData, REFRESH_INTERVAL.logs);
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
window.setTimeout(getData, 5 * interval);
|
||||
utils.setTimer(getData, 5 * REFRESH_INTERVAL.logs);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ function padNumber(num) {
|
||||
var showAlertBox = null;
|
||||
function showAlert(type, icon, title, message) {
|
||||
const options = {
|
||||
title: " <strong>" + title + "</strong><br>",
|
||||
message: message,
|
||||
title: " <strong>" + escapeHtml(title) + "</strong><br>",
|
||||
message: escapeHtml(message),
|
||||
icon: icon,
|
||||
},
|
||||
settings = {
|
||||
@@ -123,9 +123,9 @@ function showAlert(type, icon, title, message) {
|
||||
var data = JSON.parse(message);
|
||||
console.log(data); // eslint-disable-line no-console
|
||||
if (data.error !== undefined) {
|
||||
options.title = " <strong>" + data.error.message + "</strong><br>";
|
||||
options.title = " <strong>" + escapeHtml(data.error.message) + "</strong><br>";
|
||||
|
||||
if (data.error.hint !== null) options.message = data.error.hint;
|
||||
if (data.error.hint !== null) options.message = escapeHtml(data.error.hint);
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
@@ -319,6 +319,7 @@ function addFromQueryLog(domain, list) {
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
domain: domain,
|
||||
comment: "Added from Query Log",
|
||||
@@ -655,6 +656,42 @@ function loadingOverlay(reloadAfterTimeout = false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Function that calls a function only if the page is currently visible. This is
|
||||
// useful to prevent unnecessary API calls when the page is not visible (e.g.
|
||||
// when the user is on another tab).
|
||||
function callIfVisible(func) {
|
||||
if (document.hidden) {
|
||||
// Page is not visible, try again in 1 second
|
||||
window.setTimeout(callIfVisible, 1000, func);
|
||||
return;
|
||||
}
|
||||
|
||||
// Page is visible, call function instead
|
||||
func();
|
||||
}
|
||||
|
||||
// Timer that calls a function after <interval> milliseconds but only if the
|
||||
// page is currently visible. We cancel possibly running timers for the same
|
||||
// function before starting a new one to prevent multiple timers running at
|
||||
// the same time causing unnecessary identical API calls when the page is
|
||||
// visible again.
|
||||
function setTimer(func, interval) {
|
||||
// Cancel possibly running timer
|
||||
window.clearTimeout(func.timer);
|
||||
// Start new timer
|
||||
func.timer = window.setTimeout(callIfVisible, interval, func);
|
||||
}
|
||||
|
||||
// Same as setTimer() but calls the function every <interval> milliseconds
|
||||
function setInter(func, interval) {
|
||||
// Cancel possibly running timer
|
||||
window.clearTimeout(func.timer);
|
||||
// Start new timer
|
||||
func.timer = window.setTimeout(callIfVisible, interval, func);
|
||||
// Restart timer
|
||||
window.setTimeout(setInter, interval, func, interval);
|
||||
}
|
||||
|
||||
window.utils = (function () {
|
||||
return {
|
||||
escapeHtml: escapeHtml,
|
||||
@@ -689,5 +726,7 @@ window.utils = (function () {
|
||||
hexDecode: hexDecode,
|
||||
listsAlert: listAlert,
|
||||
loadingOverlay: loadingOverlay,
|
||||
setTimer: setTimer,
|
||||
setInter: setInter,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -100,7 +100,7 @@ if startsWith(scriptname, 'groups') then
|
||||
<link rel="stylesheet" href="<?=pihole.fileversion('style/vendor/bootstrap-toggle.min.css')?>">
|
||||
<? end ?>
|
||||
<? if is_authenticated then ?>
|
||||
<link rel="stylesheet" href="<?=pihole.fileversion('style/vendor/datatables.min.css')?>">
|
||||
<link rel="stylesheet" href="<?=pihole.fileversion('style/vendor/datatables-pihole.min.css')?>">
|
||||
<link rel="stylesheet" href="<?=pihole.fileversion('style/vendor/datatables_extensions.min.css')?>">
|
||||
<link rel="stylesheet" href="<?=pihole.fileversion('style/vendor/daterangepicker.min.css')?>">
|
||||
<? end ?>
|
||||
|
||||
58
scripts/vendor/datatables.buttons.min.js
vendored
58
scripts/vendor/datatables.buttons.min.js
vendored
File diff suppressed because one or more lines are too long
213
scripts/vendor/datatables.min.js
vendored
213
scripts/vendor/datatables.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -32,7 +32,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-xs-12 col-sm-6 col-md-12 col-lg-6">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">From</div>
|
||||
<div class="input-group-addon">Start</div>
|
||||
<input type="text" class="form-control DHCPgroup" id="dhcp.start" data-key="dhcp.start"
|
||||
autocomplete="off" spellcheck="false" autocapitalize="none"
|
||||
autocorrect="off" value="">
|
||||
@@ -42,7 +42,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-xs-12 col-sm-6 col-md-12 col-lg-6">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">To</div>
|
||||
<div class="input-group-addon">End</div>
|
||||
<input type="text" class="form-control DHCPgroup" id="dhcp.end" data-key="dhcp.end"
|
||||
autocomplete="off" spellcheck="false" autocapitalize="none"
|
||||
autocorrect="off" value="">
|
||||
@@ -60,6 +60,17 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-6 col-md-12 col-lg-6">
|
||||
<label>Netmask (<code>0.0.0.0</code> = auto)</label>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">Netmask</div>
|
||||
<input type="text" class="form-control DHCPgroup" id="dhcp.netmask" data-key="dhcp.netmask"
|
||||
autocomplete="off" spellcheck="false" autocapitalize="none"
|
||||
autocorrect="off" value="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div><input type="checkbox" id="dhcp.ipv6" data-key="dhcp.ipv6" class="DHCPgroup"> <label for="dhcp.ipv6"><strong>Enable additional IPv6 support (SLAAC + RA)</strong></label></div>
|
||||
<p>Enable this option to enable IPv6 support for the Pi-hole DHCP server. This will allow the Pi-hole to hand out IPv6 addresses to clients and also provide IPv6 router advertisements (RA) to clients. This option is only useful if the Pi-hole is configured with an IPv6 address.</p>
|
||||
@@ -85,7 +96,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
autocorrect="off" id="dhcp.leaseTime" data-key="dhcp.leaseTime" value="">
|
||||
</div>
|
||||
</div>
|
||||
<p>The lease time can be in seconds, minutes (e.g., "45m"), hours (e.g., "1h"), days (like "2d"), or even weeks ("1w"). You may also use "infinite" as string but be aware of the drawbacks: assigned addresses are will only be made available again after the lease time has passed or when leases are manually deleted below.</p>
|
||||
<p>The lease time can be in seconds, minutes (e.g., "45m"), hours (e.g., "1h"), days (like "2d"), or even weeks ("1w"). If no lease time is specified (empty), <code>dnsmasq</code>'s default lease time is one hour for IPv4 and one day for IPv6. You may also use "infinite" as string but be aware of the drawbacks: assigned addresses are will only be made available again after the lease time has passed or when leases are manually deleted below.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -14,125 +14,96 @@ PageTitle = "Local DNS Settings"
|
||||
mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
?>
|
||||
<div class="row">
|
||||
<div class="col-md-6 settings-level-1">
|
||||
<div class="col-md-12 col-lg-6 settings-level-1">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" id="title-hosts" data-configkeys="dns.hosts">Local DNS records</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box collapsed-box">
|
||||
<!-- /.box-header -->
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">
|
||||
Add new DNS record
|
||||
</h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" data-configkeys="hosts" data-widget="collapse"><i class="fa fa-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="domain">Domain:</label>
|
||||
<input id="Hdomain" type="url" class="form-control" data-configkeys="hosts" placeholder="Add a domain (example.com or sub.example.com)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="target">IP address:</label>
|
||||
<input id="Hip" type="text" class="form-control" data-configkeys="hosts" placeholder="Associated IP address" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer clearfix">
|
||||
<strong>Note:</strong>
|
||||
<p>Adding/removing local DNS records will flush the cache but does not require a restart of the DNS server.</p>
|
||||
<button type="button" id="btnAdd-host" class="btn btn-primary pull-right" data-configkeys="hosts">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<h3 class="box-title">List of local DNS records</h3>
|
||||
<!-- /.box-header -->
|
||||
<table id="hosts-Table" class="table table-striped table-bordered" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>IP</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>IP</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot class="add-new-item">
|
||||
<tr>
|
||||
<th>
|
||||
<input id="Hdomain" type="url" class="form-control" data-configkeys="hosts" placeholder="Domain" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</th>
|
||||
<th>
|
||||
<input id="Hip" type="text" class="form-control" data-configkeys="hosts" placeholder="Associated IP" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</th>
|
||||
<th>
|
||||
<button type="button" id="btnAdd-host" class="btn btn-primary btn-xs" data-configkeys="hosts"><i class="fa fa-plus"></i></button>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<button type="button" id="resetButton" class="btn btn-default btn-sm text-red hidden">Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<strong>Note:</strong>
|
||||
<p>Adding/removing local DNS records will flush the cache but does not require a restart of the DNS server.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 settings-level-1">
|
||||
<div class="col-md-12 col-lg-6 settings-level-1">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" id="title-cnameRecords" data-configkeys="dns.cnameRecords">Local CNAME records records</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box collapsed-box">
|
||||
<!-- /.box-header -->
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">
|
||||
Add new CNAME record
|
||||
</h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" data-configkeys="cnameRecords" data-widget="collapse"><i class="fa fa-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-5">
|
||||
<label for="domain">Domain:</label>
|
||||
<input id="Cdomain" type="url" class="form-control" data-configkeys="cnameRecords" placeholder="Add a domain (example.com or sub.example.com)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<label for="target">Target Domain:</label>
|
||||
<input id="Ctarget" type="url" class="form-control" data-configkeys="cnameRecords" placeholder="Associated Target Domain" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<label for="target">TTL (optional):</label>
|
||||
<input id="Cttl" type="numeric" class="form-control" data-configkeys="cnameRecords" placeholder="TTL in seconds" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer clearfix">
|
||||
<strong>Note:</strong>
|
||||
<p>The target of a <code>CNAME</code> must be a domain that the Pi-hole already has in its cache or is authoritative for. This is a universal limitation of <code>CNAME</code> records.</p>
|
||||
<p>The reason for this is that Pi-hole will not send additional queries upstream when serving <code>CNAME</code> replies. As consequence, if you set a target that isn't already known, the reply to the client may be incomplete. Pi-hole just returns the information it knows at the time of the query. This results in certain limitations for <code>CNAME</code> targets,
|
||||
for instance, only <i>active</i> DHCP leases work as targets - mere DHCP <i>leases</i> aren't sufficient as they aren't (yet) valid DNS records.</p>
|
||||
<p>Additionally, you can't <code>CNAME</code> external domains (<code>bing.com</code> to <code>google.com</code>) successfully as this could result in invalid SSL certificate errors when the target server does not serve content for the requested domain.</p>
|
||||
<p>Adding/removing local CNAME records will restart the DNS server.</p>
|
||||
<button type="button" id="btnAdd-cname" class="btn btn-primary pull-right" data-configkeys="cnameRecords">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<h3 class="box-title">List of local CNAME records</h3>
|
||||
<!-- /.box-header -->
|
||||
<table id="cnameRecords-Table" class="table table-striped table-bordered" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Target</th>
|
||||
<th>TTL</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Target</th>
|
||||
<th>TTL *</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot class="add-new-item">
|
||||
<tr>
|
||||
<th>
|
||||
<input id="Cdomain" type="url" class="form-control" data-configkeys="cnameRecords" placeholder="Domain" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</th>
|
||||
<th>
|
||||
<input id="Ctarget" type="url" class="form-control" data-configkeys="cnameRecords" placeholder="Target Domain" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</th>
|
||||
<th>
|
||||
<input id="Cttl" type="numeric" class="form-control" data-configkeys="cnameRecords" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</th>
|
||||
<th>
|
||||
<button type="button" id="btnAdd-cname" class="btn btn-primary btn-xs" data-configkeys="cnameRecords"><i class="fa fa-plus"></i></button>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<button type="button" id="resetButton" class="btn btn-default btn-sm text-red hidden">Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<strong>Note:</strong>
|
||||
<p>The target of a <code>CNAME</code> must be a domain that the Pi-hole already has in its cache or is authoritative for. This is a universal limitation of <code>CNAME</code> records.</p>
|
||||
<p>The reason for this is that Pi-hole will not send additional queries upstream when serving <code>CNAME</code> replies. As consequence, if you set a target that isn't already known, the reply to the client may be incomplete. Pi-hole just returns the information it knows at the time of the query. This results in certain limitations for <code>CNAME</code> targets,
|
||||
for instance, only <i>active</i> DHCP leases work as targets - mere DHCP <i>leases</i> aren't sufficient as they aren't (yet) valid DNS records.</p>
|
||||
<p>Additionally, you can't <code>CNAME</code> external domains (<code>bing.com</code> to <code>google.com</code>) successfully as this could result in invalid SSL certificate errors when the target server does not serve content for the requested domain.</p>
|
||||
<p>Adding/removing local CNAME records will restart the DNS server.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,15 +51,16 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<input type="checkbox" id="database.DBexport" data-key="database.DBexport" title="log-queries">
|
||||
<label for="database.DBexport"><strong>Should FTL store queries in the database?</strong></label>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
<div class="row-flex">
|
||||
<input type="number" id="database.maxDBdays" data-key="database.maxDBdays" data-type="integer" value="" min="0" step="10" style="width: 5em;">
|
||||
<label for="database.maxDBdays"><strong>Maximum number of days to keep queries in the database</strong></label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="row-flex">
|
||||
<input type="number" id="database.network.expire" data-key="database.network.expire" data-type="integer" value="" min="0" step="10" style="width: 5em;">
|
||||
<label for="database.network.expire"><strong>How long should IP addresses be kept in the network_addresses table [days]?</strong></label>
|
||||
<p>IP addresses (and associated host names) older than the specified number of days are removed to avoid dead entries in the network overview table.</p>
|
||||
</div>
|
||||
<p>IP addresses (and associated host names) older than the specified number of days are removed to avoid dead entries in the network overview table.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,6 +92,11 @@ td.lookatme {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Client column */
|
||||
#all-queries td:nth-of-type(5) {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Allow Info String to wrap (useful while filtering entries on small screen) */
|
||||
#all-queries_info {
|
||||
white-space: unset;
|
||||
@@ -368,12 +373,18 @@ td.lookatme {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-inline .dataTables .form-control {
|
||||
.form-inline .dataTable .form-control {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Table footer row used to add new items, using inline input fields */
|
||||
tfoot.add-new-item > tr > th {
|
||||
font-weight: normal;
|
||||
vertical-align: inherit;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results > .select2-results__options {
|
||||
max-height: 400px;
|
||||
}
|
||||
@@ -680,6 +691,11 @@ li:not(.menu-open) .treeview-menu .warning-count {
|
||||
border-color: #3c8dbc;
|
||||
}
|
||||
|
||||
/* Fix indentation of <p> elements inside icheck elements */
|
||||
[class*="icheck-"] > label + p {
|
||||
padding-left: 29px;
|
||||
}
|
||||
|
||||
/* Fix some datatables layout on small screens */
|
||||
@media screen and (max-width: 660px), screen and (min-width: 767px) and (max-width: 960px) {
|
||||
#domainsTable_wrapper .table-responsive {
|
||||
@@ -998,6 +1014,8 @@ body:not(.lcars) .filter_types [class*="icheck-"] > input:first-child:checked +
|
||||
/* Datatables Select - bgcolor */
|
||||
:root {
|
||||
--datatable-bgcolor: #e0e8ee;
|
||||
--dt-row-selected: var(--datatable-bgcolor);
|
||||
--dt-row-selected-text: currentColor;
|
||||
}
|
||||
|
||||
div.dt-buttons {
|
||||
@@ -1404,3 +1422,12 @@ table.dataTable tbody > tr > .selected {
|
||||
display: none;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* General layout - Row containing flex elements */
|
||||
.row-flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@@ -483,6 +483,11 @@ kbd {
|
||||
color: #808890;
|
||||
}
|
||||
|
||||
/* Used in error403 */
|
||||
.text-danger {
|
||||
color: #f54;
|
||||
}
|
||||
|
||||
.warning-count {
|
||||
color: #000;
|
||||
background: #f94;
|
||||
|
||||
21
style/vendor/datatables-pihole.min.css
vendored
Normal file
21
style/vendor/datatables-pihole.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
15
style/vendor/datatables.min.css
vendored
15
style/vendor/datatables.min.css
vendored
@@ -1,15 +0,0 @@
|
||||
/*
|
||||
* This combined file was created by the DataTables downloader builder:
|
||||
* https://datatables.net/download
|
||||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs/dt-1.10.21
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 1.10.21
|
||||
*/
|
||||
|
||||
table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody>table>thead .sorting:after,div.dataTables_scrollBody>table>thead .sorting_asc:after,div.dataTables_scrollBody>table>thead .sorting_desc:after{display:none}div.dataTables_scrollBody>table>tbody>tr:first-child>th,div.dataTables_scrollBody>table>tbody>tr:first-child>td{border-top:none}div.dataTables_scrollFoot>.dataTables_scrollFootInner{box-sizing:content-box}div.dataTables_scrollFoot>.dataTables_scrollFootInner>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable{border-right-width:0}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:1px}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0}
|
||||
|
||||
|
||||
8
style/vendor/datatables_extensions.min.css
vendored
8
style/vendor/datatables_extensions.min.css
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user