diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5dd1e6fa..6fff93e7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.1.0 + uses: actions/checkout@v4.1.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index ca936784..05e5593f 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.1.0 + uses: actions/checkout@v4.1.1 - name: Spell-Checking uses: codespell-project/actions-codespell@master diff --git a/.github/workflows/editorconfig-checker.yml b/.github/workflows/editorconfig-checker.yml index 54b71d90..db28dc97 100644 --- a/.github/workflows/editorconfig-checker.yml +++ b/.github/workflows/editorconfig-checker.yml @@ -9,6 +9,6 @@ jobs: name: editorconfig-checker runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.0 + - uses: actions/checkout@v4.1.1 - uses: editorconfig-checker/action-editorconfig-checker@main - run: editorconfig-checker diff --git a/.github/workflows/sync-back-to-dev.yml b/.github/workflows/sync-back-to-dev.yml index 901c057a..cb53b2f6 100644 --- a/.github/workflows/sync-back-to-dev.yml +++ b/.github/workflows/sync-back-to-dev.yml @@ -11,7 +11,7 @@ jobs: name: Syncing branches steps: - name: Checkout - uses: actions/checkout@v4.1.0 + uses: actions/checkout@v4.1.1 - name: Opening pull request run: gh pr create -B devel -H master --title 'Sync master back into development' --body 'Created by Github action' --label 'internal' env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6cde56f3..48e1c663 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,10 +19,10 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v4.1.0 + uses: actions/checkout@v4.1.1 - name: Set up Node.js - uses: actions/setup-node@v3.8.1 + uses: actions/setup-node@v4.0.0 with: node-version: "16.x" cache: npm diff --git a/index.lp b/index.lp index 22e7db5a..6d310747 100644 --- a/index.lp +++ b/index.lp @@ -22,8 +22,8 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
- - - active clients + + - active clients diff --git a/queries.lp b/queries.lp index 0414b770..d0af917c 100644 --- a/queries.lp +++ b/queries.lp @@ -165,6 +165,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r') Type Domain Client + @@ -175,6 +176,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r') Type Domain Client + @@ -192,7 +194,6 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r') - diff --git a/scripts/pi-hole/js/charts.js b/scripts/pi-hole/js/charts.js index b89681eb..e8252275 100644 --- a/scripts/pi-hole/js/charts.js +++ b/scripts/pi-hole/js/charts.js @@ -87,7 +87,6 @@ const htmlLegendPlugin = { }); } - textLink.style.color = item.fontColor; textLink.style.margin = 0; textLink.style.padding = 0; textLink.style.textDecoration = item.hidden ? "line-through" : ""; @@ -103,11 +102,11 @@ const htmlLegendPlugin = { // eslint-disable-next-line no-unused-vars var customTooltips = function (context) { var tooltip = context.tooltip; - var tooltipEl = document.getElementById(this._chart.canvas.id + "-customTooltip"); + var tooltipEl = document.getElementById(this.chart.canvas.id + "-customTooltip"); if (!tooltipEl) { // Create Tooltip Element once per chart tooltipEl = document.createElement("div"); - tooltipEl.id = this._chart.canvas.id + "-customTooltip"; + tooltipEl.id = this.chart.canvas.id + "-customTooltip"; tooltipEl.classList.add("chartjs-tooltip"); tooltipEl.innerHTML = "
"; // avoid browser's font-zoom since we know that 's @@ -121,7 +120,7 @@ var customTooltips = function (context) { tooltipEl.style.fontSize = tooltip.options.bodyFont.size / fontZoom + "px"; tooltipEl.style.fontStyle = tooltip.options.bodyFont.style; // append Tooltip next to canvas-containing box - tooltipEl.ancestor = this._chart.canvas.closest(".box[id]").parentNode; + tooltipEl.ancestor = this.chart.canvas.closest(".box[id]").parentNode; tooltipEl.ancestor.append(tooltipEl); } @@ -175,7 +174,7 @@ var customTooltips = function (context) { tableRoot.innerHTML = innerHtml; } - var canvasPos = this._chart.canvas.getBoundingClientRect(); + var canvasPos = this.chart.canvas.getBoundingClientRect(); var boxPos = tooltipEl.ancestor.getBoundingClientRect(); var offsetX = canvasPos.left - boxPos.left; var offsetY = canvasPos.top - boxPos.top; diff --git a/scripts/pi-hole/js/groups-clients.js b/scripts/pi-hole/js/groups-clients.js index 32483b4f..bb9eed94 100644 --- a/scripts/pi-hole/js/groups-clients.js +++ b/scripts/pi-hole/js/groups-clients.js @@ -405,34 +405,47 @@ function delItems(ids) { } function addClient() { - var ip = utils.escapeHtml($("#select").val().trim()); const comment = utils.escapeHtml($("#new_comment").val()); - utils.disableAll(); - utils.showAlert("info", "", "Adding client...", ip); - - if (ip.length === 0) { - utils.enableAll(); - utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address"); - return; - } + // 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,]+/); + // Remove empty elements + ips = ips.filter(function (el) { + return el !== ""; + }); + const ipStr = JSON.stringify(ips); // Validate input, can be: // - IPv4 address (with and without CIDR) // - IPv6 address (with and without CIDR) // - MAC address (in the form AA:BB:CC:DD:EE:FF) // - host name (arbitrary form, we're only checking against some reserved characters) - if (utils.validateIPv4CIDR(ip) || utils.validateIPv6CIDR(ip) || utils.validateMAC(ip)) { - // Convert input to upper case (important for MAC addresses) - ip = ip.toUpperCase(); - } else if (!utils.validateHostname(ip)) { + for (var i = 0; i < ips.length; i++) { + if ( + utils.validateIPv4CIDR(ips[i]) || + utils.validateIPv6CIDR(ips[i]) || + utils.validateMAC(ips[i]) + ) { + // Convert input to upper case (important for MAC addresses) + ips[i] = ips[i].toUpperCase(); + } else if (!utils.validateHostname(ips[i])) { + utils.showAlert( + "warning", + "", + "Warning", + "Input is neither a valid IP or MAC address nor a valid host name!" + ); + return; + } + } + + utils.disableAll(); + utils.showAlert("info", "", "Adding client(s)...", ipStr); + + if (ips.length === 0) { utils.enableAll(); - utils.showAlert( - "warning", - "", - "Warning", - "Input is neither a valid IP or MAC address nor a valid host name!" - ); + utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address"); return; } @@ -441,10 +454,10 @@ function addClient() { method: "post", dataType: "json", processData: false, - data: JSON.stringify({ client: ip, comment: comment }), - success: function () { + data: JSON.stringify({ client: ips, comment: comment }), + success: function (data) { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added client", ip); + utils.listsAlert("client", ips, data); reloadClientSuggestions(); table.ajax.reload(null, false); table.rows().deselect(); diff --git a/scripts/pi-hole/js/groups-domains.js b/scripts/pi-hole/js/groups-domains.js index d0f07e01..719a7f81 100644 --- a/scripts/pi-hole/js/groups-domains.js +++ b/scripts/pi-hole/js/groups-domains.js @@ -497,46 +497,54 @@ function addDomain() { commentEl = $("#new_regex_comment"); } - var domain = utils.escapeHtml(domainEl.val()); const comment = utils.escapeHtml(commentEl.val()); - utils.disableAll(); - utils.showAlert("info", "", "Adding domain...", domain); + // 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,]+/); + // Remove empty elements + domains = domains.filter(function (el) { + return el !== ""; + }); + const domainStr = JSON.stringify(domains); - if (domain.length < 2) { + utils.disableAll(); + utils.showAlert("info", "", "Adding domain(s)...", domainStr); + + if (domains.length === 0) { utils.enableAll(); - utils.showAlert("warning", "", "Warning", "Please specify a domain"); + utils.showAlert("warning", "", "Warning", "Please specify at least one domain"); return; } - // strip "*." if specified by user in wildcard mode - if (kind === "exact" && wildcardChecked && domain.startsWith("*.")) { - domain = domain.substr(2); + for (var i = 0; i < domains.length; i++) { + if (kind === "exact" && wildcardChecked) { + // Transform domain to wildcard if specified by user + domains[i] = "(\\.|^)" + domains[i].replaceAll(".", "\\.") + "$"; + kind = "regex"; + + // strip leading "*." if specified by user in wildcard mode + if (domains[i].startsWith("*.")) domains[i] = domains[i].substr(2); + } } // determine list type const type = action === "add_deny" ? "deny" : "allow"; - // Transform domain to wildcard if specified by user - if (kind === "exact" && wildcardChecked) { - domain = "(\\.|^)" + domain.replaceAll(".", "\\.") + "$"; - kind = "regex"; - } - $.ajax({ url: "/api/domains/" + type + "/" + kind, method: "post", dataType: "json", processData: false, data: JSON.stringify({ - domain: domain, + domain: domains, comment: comment, type: type, kind: kind, }), - success: function () { + success: function (data) { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added domain", domain); + utils.listsAlert("domain", domains, data); table.ajax.reload(null, false); table.rows().deselect(); diff --git a/scripts/pi-hole/js/groups-lists.js b/scripts/pi-hole/js/groups-lists.js index 2ac98181..ff85ec24 100644 --- a/scripts/pi-hole/js/groups-lists.js +++ b/scripts/pi-hole/js/groups-lists.js @@ -499,13 +499,21 @@ function delItems(ids) { function addList(event) { const type = event.data.type; - const address = utils.escapeHtml($("#new_address").val()); const comment = utils.escapeHtml($("#new_comment").val()); - utils.disableAll(); - utils.showAlert("info", "", "Adding subscribed " + type + "list...", address); + // 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,]+/); + // Remove empty elements + addresses = addresses.filter(function (el) { + return el !== ""; + }); + const addressestr = JSON.stringify(addresses); - if (address.length === 0) { + utils.disableAll(); + utils.showAlert("info", "", "Adding subscribed " + type + "list(s)...", addressestr); + + if (addresses.length === 0) { // enable the ui elements again utils.enableAll(); utils.showAlert("warning", "", "Warning", "Please specify " + type + "list address"); @@ -517,10 +525,10 @@ function addList(event) { method: "post", dataType: "json", processData: false, - data: JSON.stringify({ address: address, comment: comment, type: type }), - success: function () { + data: JSON.stringify({ address: addresses, comment: comment, type: type }), + success: function (data) { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added " + type + "list", address); + utils.listsAlert("list", addresses, data); table.ajax.reload(null, false); table.rows().deselect(); diff --git a/scripts/pi-hole/js/groups.js b/scripts/pi-hole/js/groups.js index fdb9ed36..f05d52bf 100644 --- a/scripts/pi-hole/js/groups.js +++ b/scripts/pi-hole/js/groups.js @@ -277,13 +277,24 @@ function delItems(ids) { } function addGroup() { - const name = utils.escapeHtml($("#new_name").val()); const comment = utils.escapeHtml($("#new_comment").val()); - utils.disableAll(); - utils.showAlert("info", "", "Adding group...", name); + // Check if the user wants to add multiple groups (space or newline separated) + // If so, split the input and store it in an array + var names = utils + .escapeHtml($("#new_name")) + .val() + .split(/[\s,]+/); + // Remove empty elements + names = names.filter(function (el) { + return el !== ""; + }); + const groupStr = JSON.stringify(names); - if (name.length === 0) { + utils.disableAll(); + utils.showAlert("info", "", "Adding group(s)...", groupStr); + + if (names.length === 0) { // enable the ui elements again utils.enableAll(); utils.showAlert("warning", "", "Warning", "Please specify a group name"); @@ -296,13 +307,13 @@ function addGroup() { dataType: "json", processData: false, data: JSON.stringify({ - name: name, + name: names, comment: comment, enabled: true, }), - success: function () { + success: function (data) { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added group", name); + utils.listsAlert("group", names, data); $("#new_name").val(""); $("#new_comment").val(""); table.ajax.reload(); diff --git a/scripts/pi-hole/js/index.js b/scripts/pi-hole/js/index.js index 814ad143..0582ce0b 100644 --- a/scripts/pi-hole/js/index.js +++ b/scripts/pi-hole/js/index.js @@ -16,6 +16,13 @@ var queryTypePieChart, forwardDestinationPieChart; var failures = 0; function updateQueriesOverTime() { $.getJSON("/api/history", function (data) { + // Remove graph if there are no results (e.g. new + // installation or privacy mode enabled) + if (jQuery.isEmptyObject(data.history)) { + $("#queries-over-time").remove(); + return; + } + // Remove possibly already existing data timeLineChart.data.labels = []; timeLineChart.data.datasets = []; @@ -123,13 +130,10 @@ function updateClientsOverTime() { // Remove graph if there are no results (e.g. new // installation or privacy mode enabled) if (jQuery.isEmptyObject(data.history)) { - $("#clients-over-time").parent().remove(); + $("#clients").remove(); return; } - // remove last data point for line charts as it is not representative there - if (utils.getGraphType() === "line") data.history.splice(-1, 1); - var i, labels = []; data.clients.forEach(function (client) { @@ -384,7 +388,11 @@ function updateSummaryData(runOnce) { $.getJSON("/api/stats/summary", function (data) { var intl = new Intl.NumberFormat(); glowIfChanged($("span#dns_queries"), intl.format(parseInt(data.queries.total, 10))); - glowIfChanged($("span#total_clients"), intl.format(parseInt(data.clients.total, 10))); + glowIfChanged($("span#active_clients"), intl.format(parseInt(data.clients.active, 10))); + $("a#total_clients").attr( + "title", + intl.format(parseInt(data.clients.total, 10)) + " total clients" + ); glowIfChanged($("span#blocked_queries"), intl.format(parseFloat(data.queries.blocked))); glowIfChanged( $("span#percent_blocked"), @@ -416,7 +424,7 @@ $(function () { var ticksColor = utils.getCSSval("graphs-ticks", "color"); var ctx = document.getElementById("queryOverTimeChart").getContext("2d"); timeLineChart = new Chart(ctx, { - type: utils.getGraphType(), + type: "bar", data: { labels: [], datasets: [{ data: [], parsing: false }], @@ -471,7 +479,7 @@ $(function () { }, }, scales: { - xAxes: { + x: { type: "time", stacked: true, offset: false, @@ -491,7 +499,7 @@ $(function () { color: ticksColor, }, }, - yAxes: { + y: { stacked: true, beginAtZero: true, ticks: { @@ -527,7 +535,7 @@ $(function () { if (clientsChartEl) { ctx = clientsChartEl.getContext("2d"); clientsChart = new Chart(ctx, { - type: utils.getGraphType(), + type: "bar", data: { labels: [], datasets: [{ data: [], parsing: false }], @@ -566,7 +574,7 @@ $(function () { }, }, scales: { - xAxes: { + x: { type: "time", stacked: true, offset: false, @@ -586,7 +594,7 @@ $(function () { color: ticksColor, }, }, - yAxes: { + y: { beginAtZero: true, ticks: { color: ticksColor, diff --git a/scripts/pi-hole/js/queries.js b/scripts/pi-hole/js/queries.js index 4be81386..cc169a73 100644 --- a/scripts/pi-hole/js/queries.js +++ b/scripts/pi-hole/js/queries.js @@ -208,6 +208,23 @@ function parseQueryStatus(data) { }; } +function formatReplyTime(replyTime, type) { + if (type === "display") { + // Units: + // - seconds if replytime >= 1 second + // - milliseconds if reply time >= 100 µs + // - microseconds otherwise + return replyTime < 1e-4 + ? (1e6 * replyTime).toFixed(1) + " µs" + : replyTime < 1 + ? (1e3 * replyTime).toFixed(1) + " ms" + : replyTime.toFixed(1) + " s"; + } + + // else: return the number itself (for sorting and searching) + return replyTime; +} + function formatInfo(data) { // DNSSEC status var dnssecStatus = data.dnssec, @@ -303,20 +320,10 @@ function formatInfo(data) { // Always show reply info, add reply delay if applicable var replyInfo = ""; - if (data.reply.type !== "UNKNOWN") { - replyInfo = divStart + "Reply:  " + data.reply.type; - if (data.reply.time >= 0 && data.reply.type !== "UNKNOWN") { - replyInfo += - " (" + - (data.reply.time < 1 - ? (1e3 * data.reply.time).toFixed(1) + " ms)" - : data.reply.time.toFixed(1) + " s)"); - } - - replyInfo += ""; - } else { - replyInfo = divStart + "Reply:  No reply received"; - } + replyInfo = + data.reply.type !== "UNKNOWN" + ? divStart + "Reply:  " + data.reply.type + "" + : divStart + "Reply:  No reply received"; // Compile extra info for displaying return ( @@ -515,9 +522,10 @@ $(function () { }, }, { data: "status", width: "1%" }, - { data: "type", width: "5%" }, + { data: "type", width: "1%" }, { data: "domain", width: "45%" }, - { data: "client.ip", width: "29%", type: "ip-address", render: $.fn.dataTable.render.text() }, + { data: "client.ip", width: "29%", type: "ip-address" }, + { data: "reply.time", width: "4%", render: formatReplyTime }, { data: null, width: "10%", sortable: false, searchable: false }, ], lengthMenu: [ @@ -569,7 +577,7 @@ $(function () { } if (querystatus.buttontext !== false) { - $("td:eq(5)", row).html(querystatus.buttontext); + $("td:eq(6)", row).html(querystatus.buttontext); } }, }); diff --git a/scripts/pi-hole/js/settings-api.js b/scripts/pi-hole/js/settings-api.js index d2e80cc8..e498f297 100644 --- a/scripts/pi-hole/js/settings-api.js +++ b/scripts/pi-hole/js/settings-api.js @@ -9,6 +9,7 @@ var apiSessionsTable = null; var ownSessionID = null; +var deleted = 0; var TOTPdata = null; function renderBool(data, type) { @@ -60,7 +61,7 @@ $(function () { }, ], drawCallback: function () { - $('button[id^="deleteSession_"]').on("click", deleteSession); + $('button[id^="deleteSession_"]').on("click", deleteThisSession); // Hide buttons if all messages were deleted var hasRows = this.api().rows({ filter: "applied" }).data().length > 0; @@ -70,7 +71,7 @@ $(function () { $("body > .bootstrap-select.dropdown").remove(); }, rowCallback: function (row, data) { - $(row).attr("data-id", data.ip); + $(row).attr("data-id", data.id); var button = '"; $("td:eq(2)", row).html(button); - $("td:eq(0)", row).text(name); - $("td:eq(1)", row).text(ip); }, dom: "<'row'<'col-sm-6'l><'col-sm-6'f>>" + @@ -91,9 +114,9 @@ $(function () { }, order: [[0, "asc"]], columns: [ - { data: null }, - { data: null }, - { data: null }, + { data: null, render: CNAMEdomain }, + { data: null, render: CNAMEtarget }, + { data: null, render: CNAMEttl }, { data: null, width: "22px", orderable: false }, ], columnDefs: [ @@ -109,9 +132,6 @@ $(function () { $("body > .bootstrap-select.dropdown").remove(); }, rowCallback: function (row, data) { - // Split record in format ,[,] - var CNAMEarr = data.split(","); - $(row).attr("data-id", data); var button = '"; $("td:eq(3)", row).html(button); - $("td:eq(0)", row).text(CNAMEarr[0]); - $("td:eq(1)", row).text(CNAMEarr[1]); - if (CNAMEarr.length > 2) $("td:eq(2)", row).text(CNAMEarr[2]); - else $("td:eq(2)", row).text("-"); }, dom: "<'row'<'col-sm-6'l><'col-sm-6'f>>" + @@ -230,15 +246,17 @@ function delCNAME(elem) { $(document).ready(function () { $("#btnAdd-host").on("click", function () { + utils.disableAll(); const elem = $("#Hip").val() + " " + $("#Hdomain").val(); const url = "/api/config/dns/hosts/" + encodeURIComponent(elem); + utils.showAlert("info", "", "Adding DNS record...", elem); $.ajax({ url: url, method: "PUT", }) .done(function () { utils.enableAll(); - utils.showAlert("success", "far fa-plus", "Successfully added DNS record", ""); + utils.showAlert("success", "fas fa-plus", "Successfully added DNS record", elem); dnsRecordsTable.ajax.reload(null, false); }) .fail(function (data, exception) { @@ -250,17 +268,19 @@ $(document).ready(function () { }); $("#btnAdd-cname").on("click", function () { + utils.disableAll(); var elem = $("#Cdomain").val() + "," + $("#Ctarget").val(); var ttlVal = parseInt($("#Cttl").val(), 10); if (isFinite(ttlVal) && ttlVal >= 0) elem += "," + ttlVal; const url = "/api/config/dns/cnameRecords/" + encodeURIComponent(elem); + utils.showAlert("info", "", "Adding DNS record...", elem); $.ajax({ url: url, method: "PUT", }) .done(function () { utils.enableAll(); - utils.showAlert("success", "far fa-plus", "Successfully added CNAME record", ""); + utils.showAlert("success", "fas fa-plus", "Successfully added CNAME record", elem); dnsRecordsTable.ajax.reload(null, false); }) .fail(function (data, exception) { diff --git a/scripts/pi-hole/js/settings-teleporter.js b/scripts/pi-hole/js/settings-teleporter.js index a1dbacee..5c0f0871 100644 --- a/scripts/pi-hole/js/settings-teleporter.js +++ b/scripts/pi-hole/js/settings-teleporter.js @@ -33,6 +33,7 @@ function importZIP() { fetch("/api/teleporter", { method: "POST", body: formData, + headers: { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content") }, }) .then(response => response.json()) .then(data => { @@ -70,3 +71,27 @@ function importZIP() { console.error(error); // eslint-disable-line no-console }); } + +// Inspired by https://stackoverflow.com/a/59576416/2087442 +$("#GETTeleporter").on("click", function () { + $.ajax({ + url: "/api/teleporter", + headers: { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content") }, + method: "GET", + xhrFields: { + responseType: "blob", + }, + success: function (data, status, xhr) { + var a = document.createElement("a"); + // eslint-disable-next-line compat/compat + var url = window.URL.createObjectURL(data); + a.href = url; + a.download = xhr.getResponseHeader("Content-Disposition").match(/filename="([^"]*)"/)[1]; + document.body.append(a); + a.click(); + a.remove(); + // eslint-disable-next-line compat/compat + window.URL.revokeObjectURL(url); + }, + }); +}); diff --git a/scripts/pi-hole/js/utils.js b/scripts/pi-hole/js/utils.js index b0795ff1..b3c87040 100644 --- a/scripts/pi-hole/js/utils.js +++ b/scripts/pi-hole/js/utils.js @@ -83,63 +83,51 @@ function padNumber(num) { return ("00" + num).substr(-2, 2); } -var info = null; +var showAlertBox = null; function showAlert(type, icon, title, message) { - var opts = {}; - title = " " + title + "
"; + const options = { + title: " " + title + "
", + message: message, + }, + settings = { + type: type, + delay: 5000, // default value + mouse_over: "pause", + }; switch (type) { case "info": - opts = { - type: "info", - icon: "far fa-clock", - title: title, - message: message, - }; - info = $.notify(opts); - break; - case "success": - opts = { - type: "success", - icon: icon, - title: title, - message: message, - }; - if (info) { - info.update(opts); - } else { - $.notify(opts); - } + options.icon = icon !== null && icon.len > 0 ? icon : "far fa-clock"; + break; + case "success": break; case "warning": - opts = { - type: "warning", - icon: "fas fa-exclamation-triangle", - title: title, - message: message, - }; - if (info) { - info.update(opts); - } else { - $.notify(opts); - } + options.icon = "fas fa-exclamation-triangle"; + settings.delay *= 2; break; case "error": - opts = { - type: "danger", - icon: "fas fa-times", - title: " Error, something went wrong!
", - message: message, - }; - if (info) { - info.update(opts); - } else { - $.notify(opts); - } + options.icon = "fas fa-times"; + options.title = " Error, something went wrong!
"; + settings.delay *= 2; break; default: + // Case not handled, do nothing + console.log("Unknown alert type: " + type); // eslint-disable-line no-console + return; + } + + if (type === "info") { + // Create a new notification for info boxes + showAlertBox = $.notify(options, settings); + } else if (showAlertBox !== null) { + // Update existing notification for other boxes (if available) + showAlertBox.update(options); + showAlertBox.update(settings); + } else { + // Create a new notification for other boxes if no previous info box exists + $.notify(options, settings); } } @@ -283,11 +271,6 @@ function stateLoadCallback(itemName) { return data; } -function getGraphType() { - // Only return line if `barchart_chkbox` is explicitly set to false. Else return bar - return localStorage && localStorage.getItem("barchart_chkbox") === "false" ? "line" : "bar"; -} - function addFromQueryLog(domain, list) { var alertModal = $("#alertModal"); var alProcessing = alertModal.find(".alProcessing"); @@ -537,6 +520,81 @@ function hexDecode(string) { return back; } +function listAlert(type, items, data) { + // Show simple success message if there is no "processed" object in "data" or + // if all items were processed successfully + if (data.processed === undefined || data.processed.success.length === items.length) { + showAlert( + "success", + "fas fa-plus", + "Successfully added " + type + (items.length !== 1 ? "s" : ""), + items.join(", ") + ); + return; + } + + // Show a more detailed message if there is a "processed" object in "data" and + // not all items were processed successfully + let message = ""; + + // Show a list of successful items if there are any + if (data.processed.success.length > 0) { + message += + "Successfully added " + + data.processed.success.length + + " " + + type + + (data.processed.success.length !== 1 ? "s" : "") + + ":"; + + // Loop over data.processed.success and print "item" + for (const item in data.processed.success) { + if (Object.prototype.hasOwnProperty.call(data.processed.success, item)) { + message += "
- " + data.processed.success[item].item + ""; + } + } + } + + // Add a line break if there are both successful and failed items + if (data.processed.success.length > 0 && data.processed.errors.length > 0) { + message += "

"; + } + + // Show a list of failed items if there are any + if (data.processed.errors.length > 0) { + message += + "Failed to add " + + data.processed.errors.length + + " " + + type + + (data.processed.errors.length !== 1 ? "s" : "") + + ":\n"; + + // Loop over data.processed.errors and print "item: error" + for (const item in data.processed.errors) { + if (Object.prototype.hasOwnProperty.call(data.processed.errors, item)) { + let error = data.processed.errors[item].error; + // Replace some error messages with a more user-friendly text + if (error.indexOf("UNIQUE constraint failed") > -1) { + error = "Already present"; + } + + message += "
- " + data.processed.errors[item].item + ": " + error; + } + } + } + + // Show the warning message + const total = data.processed.success.length + data.processed.errors.length; + const processed = "(" + total + " " + type + (total !== 1 ? "s" : "") + " processed)"; + showAlert( + "warning", + "fas fa-exclamation-triangle", + "Some " + type + (items.length !== 1 ? "s" : "") + " could not be added " + processed, + message + ); +} + window.utils = (function () { return { escapeHtml: escapeHtml, @@ -553,7 +611,6 @@ window.utils = (function () { setBsSelectDefaults: setBsSelectDefaults, stateSaveCallback: stateSaveCallback, stateLoadCallback: stateLoadCallback, - getGraphType: getGraphType, validateMAC: validateMAC, validateHostname: validateHostname, addFromQueryLog: addFromQueryLog, @@ -570,5 +627,6 @@ window.utils = (function () { parseQueryString: parseQueryString, hexEncode: hexEncode, hexDecode: hexDecode, + listsAlert: listAlert, }; })(); diff --git a/scripts/pi-hole/lua/header_authenticated.lp b/scripts/pi-hole/lua/header_authenticated.lp index a85d957f..c2333f02 100644 --- a/scripts/pi-hole/lua/header_authenticated.lp +++ b/scripts/pi-hole/lua/header_authenticated.lp @@ -13,8 +13,8 @@ mg.include('header.lp','r') + - @@ -34,7 +34,7 @@ mg.include('header.lp','r')
-
diff --git a/style/pi-hole.css b/style/pi-hole.css index 71765cb2..5d73e53d 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -80,13 +80,13 @@ td.lookatme { /* Optimize Queries-Table for small screens */ /* Time column */ #all-queries td:nth-of-type(1), -/* Status column */ -#all-queries td:nth-of-type(5) { +/* Reply time column */ +#all-queries td:nth-of-type(6) { white-space: nowrap; } /* Domain column */ -#all-queries td:nth-of-type(3) { +#all-queries td:nth-of-type(4) { min-width: 200px; word-break: break-all; white-space: pre-wrap; diff --git a/style/themes/default-auto.css b/style/themes/default-auto.css index 1c1d2529..0926ca5a 100644 --- a/style/themes/default-auto.css +++ b/style/themes/default-auto.css @@ -1,6 +1,6 @@ /* Code courtesy of https://blog.jim-nielsen.com/2019/conditional-syntax-highlighting-in-dark-mode-with-css-imports/ */ -/* Assume light mode by default */ -@import "default-light.css" screen; -/* Supersede dark mode when applicable */ +/* Import light mode if color-scheme is different than dark (even not set) */ +@import "default-light.css" screen and not (prefers-color-scheme: dark); +/* Import dark mode when applicable */ @import "default-dark.css" screen and (prefers-color-scheme: dark);