diff --git a/index.lp b/index.lp index 91cec9a7..712dd238 100644 --- a/index.lp +++ b/index.lp @@ -275,6 +275,7 @@ 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 new file mode 100644 index 00000000..634193c4 --- /dev/null +++ b/scripts/pi-hole/js/charts.js @@ -0,0 +1,343 @@ +/* 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. */ + +/* global querytypeids:false */ + +// eslint-disable-next-line no-unused-vars +var THEME_COLORS = [ + "#f56954", + "#3c8dbc", + "#00a65a", + "#00c0ef", + "#f39c12", + "#0073b7", + "#001f3f", + "#39cccc", + "#3d9970", + "#01ff70", + "#ff851b", + "#f012be", + "#8e24aa", + "#d81b60", + "#222222", + "#d2d6de", +]; + +// eslint-disable-next-line no-unused-vars +const htmlLegendPlugin = { + id: "htmlLegend", + afterUpdate(chart, args, options) { + const ul = getOrCreateLegendList(chart, options.containerID); + + // Remove old legend items + while (ul.firstChild) { + ul.firstChild.remove(); + } + + // Reuse the built-in legendItems generator + const items = chart.options.plugins.legend.labels.generateLabels(chart); + + items.forEach(item => { + const li = document.createElement("li"); + li.style.alignItems = "center"; + li.style.cursor = "pointer"; + li.style.display = "flex"; + li.style.flexDirection = "row"; + + // Color checkbox (toggle visibility) + const boxSpan = document.createElement("span"); + boxSpan.title = "Toggle visibility"; + boxSpan.style.color = item.fillStyle; + boxSpan.style.display = "inline-block"; + boxSpan.style.margin = "0 10px"; + boxSpan.innerHTML = item.hidden + ? '' + : ''; + + boxSpan.addEventListener("click", () => { + const { type } = chart.config; + + if (type === "pie" || type === "doughnut") { + // Pie and doughnut charts only have a single dataset and visibility is per item + chart.toggleDataVisibility(item.index); + } else { + chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); + } + + chart.update(); + }); + + const textLink = document.createElement("p"); + if ( + chart.canvas.id === "queryTypePieChart" || + chart.canvas.id === "forwardDestinationPieChart" + ) { + // Text (link to query log page) + textLink.title = "List " + item.text + " queries"; + + textLink.addEventListener("click", () => { + if (chart.canvas.id === "queryTypePieChart") { + window.location.href = "queries.lp?querytype=" + querytypeids[item.index]; + } else if (chart.canvas.id === "forwardDestinationPieChart") { + window.location.href = "queries.lp?forwarddest=" + encodeURIComponent(item.text); + } + }); + } + + textLink.style.color = item.fontColor; + textLink.style.margin = 0; + textLink.style.padding = 0; + textLink.style.textDecoration = item.hidden ? "line-through" : ""; + textLink.className = "legend-label-text"; + textLink.append(item.text); + + li.append(boxSpan, textLink); + ul.append(li); + }); + }, +}; + +// eslint-disable-next-line no-unused-vars +var customTooltips = function (context) { + var tooltip = context.tooltip; + 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.classList.add("chartjs-tooltip"); + tooltipEl.innerHTML = "
"; + // avoid browser's font-zoom since we know that 's + // font-size was set to 14px by bootstrap's css + var fontZoom = parseFloat($("body").css("font-size")) / 14; + // set styles and font + tooltipEl.style.padding = tooltip.options.padding + "px " + tooltip.options.padding + "px"; + tooltipEl.style.borderRadius = tooltip.options.cornerRadius + "px"; + tooltipEl.style.font = tooltip.options.bodyFont.string; + tooltipEl.style.fontFamily = tooltip.options.bodyFont.family; + 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.append(tooltipEl); + } + + // Hide if no tooltip + if (tooltip.opacity === 0) { + tooltipEl.style.opacity = 0; + return; + } + + // Set caret position + tooltipEl.classList.remove("left", "right", "center", "top", "bottom"); + tooltipEl.classList.add(tooltip.xAlign, tooltip.yAlign); + + // Set Text + if (tooltip.body) { + var titleLines = tooltip.title || []; + var bodyLines = tooltip.body.map(function (bodyItem) { + return bodyItem.lines; + }); + var innerHtml = ""; + + titleLines.forEach(function (title) { + innerHtml += "" + title + ""; + }); + innerHtml += ""; + var printed = 0; + + var devicePixel = (1 / window.devicePixelRatio).toFixed(1); + bodyLines.forEach(function (body, i) { + var labelColors = tooltip.labelColors[i]; + var style = "background-color: " + labelColors.backgroundColor; + style += "; outline: 1px solid " + labelColors.backgroundColor; + style += "; border: " + devicePixel + "px solid #fff"; + var span = ""; + + var num = body[0].split(": "); + // do not display entries with value of 0 (in bar chart), + // but pass through entries with "0.0% (in pie charts) + if (num[1] !== "0") { + innerHtml += "" + span + body + ""; + printed++; + } + }); + if (printed < 1) { + innerHtml += "No activity recorded"; + } + + innerHtml += ""; + + var tableRoot = tooltipEl.querySelector("table"); + tableRoot.innerHTML = innerHtml; + } + + var canvasPos = this._chart.canvas.getBoundingClientRect(); + var boxPos = tooltipEl.ancestor.getBoundingClientRect(); + var offsetX = canvasPos.left - boxPos.left; + var offsetY = canvasPos.top - boxPos.top; + var tooltipWidth = tooltipEl.offsetWidth; + var tooltipHeight = tooltipEl.offsetHeight; + var caretX = tooltip.caretX; + var caretY = tooltip.caretY; + var caretPadding = tooltip.options.caretPadding; + var tooltipX, tooltipY, arrowX; + var arrowMinIndent = 2 * tooltip.options.cornerRadius; + var arrowSize = 5; + + // Compute X position + if ($(document).width() > 2 * tooltip.width || tooltip.xAlign !== "center") { + // If the viewport is wide enough, let the tooltip follow the caret position + tooltipX = offsetX + caretX; + if (tooltip.yAlign === "top" || tooltip.yAlign === "bottom") { + switch (tooltip.xAlign) { + case "center": + // set a minimal X position to 5px to prevent + // the tooltip to stick out left of the viewport + var minX = 5; + if (2 * tooltipX < tooltipWidth + minX) { + arrowX = tooltipX - minX; + tooltipX = minX; + } else { + tooltipX -= tooltipWidth / 2; + } + + break; + case "left": + tooltipX -= arrowMinIndent; + arrowX = arrowMinIndent; + break; + case "right": + tooltipX -= tooltipWidth - arrowMinIndent; + arrowX = tooltipWidth - arrowMinIndent; + break; + default: + break; + } + } else if (tooltip.yAlign === "center") { + switch (tooltip.xAlign) { + case "left": + tooltipX += caretPadding; + break; + case "right": + tooltipX -= tooltipWidth - caretPadding; + break; + case "center": + tooltipX -= tooltipWidth / 2; + break; + default: + break; + } + } + } else { + // compute the tooltip's center inside ancestor element + tooltipX = (tooltipEl.ancestor.offsetWidth - tooltipWidth) / 2; + // move the tooltip if the arrow would stick out to the left + if (offsetX + caretX - arrowMinIndent < tooltipX) { + tooltipX = offsetX + caretX - arrowMinIndent; + } + + // move the tooltip if the arrow would stick out to the right + if (offsetX + caretX - tooltipWidth + arrowMinIndent > tooltipX) { + tooltipX = offsetX + caretX - tooltipWidth + arrowMinIndent; + } + + arrowX = offsetX + caretX - tooltipX; + } + + // Compute Y position + switch (tooltip.yAlign) { + case "top": + tooltipY = offsetY + caretY + arrowSize + caretPadding; + break; + case "center": + tooltipY = offsetY + caretY - tooltipHeight / 2; + if (tooltip.xAlign === "left") { + tooltipX += arrowSize; + } else if (tooltip.xAlign === "right") { + tooltipX -= arrowSize; + } + + break; + case "bottom": + tooltipY = offsetY + caretY - tooltipHeight - arrowSize - caretPadding; + break; + default: + break; + } + + // Position tooltip and display + tooltipEl.style.top = tooltipY.toFixed(1) + "px"; + tooltipEl.style.left = tooltipX.toFixed(1) + "px"; + if (arrowX === undefined) { + tooltipEl.querySelector(".arrow").style.left = ""; + } else { + // Calculate percentage X value depending on the tooltip's + // width to avoid hanging arrow out on tooltip width changes + var arrowXpercent = ((100 / tooltipWidth) * arrowX).toFixed(1); + tooltipEl.querySelector(".arrow").style.left = arrowXpercent + "%"; + } + + tooltipEl.style.opacity = 1; +}; + +// eslint-disable-next-line no-unused-vars +function doughnutTooltip(tooltipLabel) { + var percentageTotalShown = tooltipLabel.chart._metasets[0].total.toFixed(1); + // tooltipLabel.chart._metasets[0].total returns the total percentage of the shown slices + // to compensate rounding errors we round to one decimal + + var label = " " + tooltipLabel.label; + var itemPercentage; + + // if we only show < 1% percent of all, show each item with two decimals + if (percentageTotalShown < 1) { + itemPercentage = tooltipLabel.parsed.toFixed(2); + } else { + // show with one decimal, but in case the item share is really small it could be rounded to 0.0 + // we compensate for this + itemPercentage = + tooltipLabel.parsed.toFixed(1) === "0.0" ? "< 0.1" : tooltipLabel.parsed.toFixed(1); + } + + // even if no doughnut slice is hidden, sometimes percentageTotalShown is slightly less then 100 + // we therefore use 99.9 to decide if slices are hidden (we only show with 0.1 precision) + if (percentageTotalShown > 99.9) { + // All items shown + return label + ": " + itemPercentage + "%"; + } else { + // set percentageTotalShown again without rounding to account + // for cases where the total shown percentage would be <0.1% of all + percentageTotalShown = tooltipLabel.chart._metasets[0].total; + return ( + label + + ":
• " + + itemPercentage + + "% of all data
• " + + ((tooltipLabel.parsed * 100) / percentageTotalShown).toFixed(1) + + "% of shown items" + ); + } +} + +// chartjs plugin used by the custom doughnut legend +const getOrCreateLegendList = (chart, id) => { + const legendContainer = document.getElementById(id); + let listContainer = legendContainer.querySelector("ul"); + + if (!listContainer) { + listContainer = document.createElement("ul"); + listContainer.style.display = "flex"; + listContainer.style.flexDirection = "column"; + listContainer.style.margin = 0; + listContainer.style.padding = 0; + + legendContainer.append(listContainer); + } + + return listContainer; +}; diff --git a/scripts/pi-hole/js/index.js b/scripts/pi-hole/js/index.js index 98c7e9ee..57a0d63a 100644 --- a/scripts/pi-hole/js/index.js +++ b/scripts/pi-hole/js/index.js @@ -5,214 +5,12 @@ * 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 */ +/* global utils:false, Chart:false, apiFailure:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false */ // Define global variables var timeLineChart, clientsChart; var queryTypePieChart, forwardDestinationPieChart; -var THEME_COLORS = [ - "#f56954", - "#3c8dbc", - "#00a65a", - "#00c0ef", - "#f39c12", - "#0073b7", - "#001f3f", - "#39cccc", - "#3d9970", - "#01ff70", - "#ff851b", - "#f012be", - "#8e24aa", - "#d81b60", - "#222222", - "#d2d6de", -]; - -var customTooltips = function (context) { - var tooltip = context.tooltip; - 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.classList.add("chartjs-tooltip"); - tooltipEl.innerHTML = "
"; - // avoid browser's font-zoom since we know that 's - // font-size was set to 14px by bootstrap's css - var fontZoom = parseFloat($("body").css("font-size")) / 14; - // set styles and font - tooltipEl.style.padding = tooltip.options.padding + "px " + tooltip.options.padding + "px"; - tooltipEl.style.borderRadius = tooltip.options.cornerRadius + "px"; - tooltipEl.style.font = tooltip.options.bodyFont.string; - tooltipEl.style.fontFamily = tooltip.options.bodyFont.family; - 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.append(tooltipEl); - } - - // Hide if no tooltip - if (tooltip.opacity === 0) { - tooltipEl.style.opacity = 0; - return; - } - - // Set caret position - tooltipEl.classList.remove("left", "right", "center", "top", "bottom"); - tooltipEl.classList.add(tooltip.xAlign, tooltip.yAlign); - - // Set Text - if (tooltip.body) { - var titleLines = tooltip.title || []; - var bodyLines = tooltip.body.map(function (bodyItem) { - return bodyItem.lines; - }); - var innerHtml = ""; - - titleLines.forEach(function (title) { - innerHtml += "" + title + ""; - }); - innerHtml += ""; - var printed = 0; - - var devicePixel = (1 / window.devicePixelRatio).toFixed(1); - bodyLines.forEach(function (body, i) { - var labelColors = tooltip.labelColors[i]; - var style = "background-color: " + labelColors.backgroundColor; - style += "; outline: 1px solid " + labelColors.backgroundColor; - style += "; border: " + devicePixel + "px solid #fff"; - var span = ""; - - var num = body[0].split(": "); - // do not display entries with value of 0 (in bar chart), - // but pass through entries with "0.0% (in pie charts) - if (num[1] !== "0") { - innerHtml += "" + span + body + ""; - printed++; - } - }); - if (printed < 1) { - innerHtml += "No activity recorded"; - } - - innerHtml += ""; - - var tableRoot = tooltipEl.querySelector("table"); - tableRoot.innerHTML = innerHtml; - } - - var canvasPos = this._chart.canvas.getBoundingClientRect(); - var boxPos = tooltipEl.ancestor.getBoundingClientRect(); - var offsetX = canvasPos.left - boxPos.left; - var offsetY = canvasPos.top - boxPos.top; - var tooltipWidth = tooltipEl.offsetWidth; - var tooltipHeight = tooltipEl.offsetHeight; - var caretX = tooltip.caretX; - var caretY = tooltip.caretY; - var caretPadding = tooltip.options.caretPadding; - var tooltipX, tooltipY, arrowX; - var arrowMinIndent = 2 * tooltip.options.cornerRadius; - var arrowSize = 5; - - // Compute X position - if ($(document).width() > 2 * tooltip.width || tooltip.xAlign !== "center") { - // If the viewport is wide enough, let the tooltip follow the caret position - tooltipX = offsetX + caretX; - if (tooltip.yAlign === "top" || tooltip.yAlign === "bottom") { - switch (tooltip.xAlign) { - case "center": - // set a minimal X position to 5px to prevent - // the tooltip to stick out left of the viewport - var minX = 5; - if (2 * tooltipX < tooltipWidth + minX) { - arrowX = tooltipX - minX; - tooltipX = minX; - } else { - tooltipX -= tooltipWidth / 2; - } - - break; - case "left": - tooltipX -= arrowMinIndent; - arrowX = arrowMinIndent; - break; - case "right": - tooltipX -= tooltipWidth - arrowMinIndent; - arrowX = tooltipWidth - arrowMinIndent; - break; - default: - break; - } - } else if (tooltip.yAlign === "center") { - switch (tooltip.xAlign) { - case "left": - tooltipX += caretPadding; - break; - case "right": - tooltipX -= tooltipWidth - caretPadding; - break; - case "center": - tooltipX -= tooltipWidth / 2; - break; - default: - break; - } - } - } else { - // compute the tooltip's center inside ancestor element - tooltipX = (tooltipEl.ancestor.offsetWidth - tooltipWidth) / 2; - // move the tooltip if the arrow would stick out to the left - if (offsetX + caretX - arrowMinIndent < tooltipX) { - tooltipX = offsetX + caretX - arrowMinIndent; - } - - // move the tooltip if the arrow would stick out to the right - if (offsetX + caretX - tooltipWidth + arrowMinIndent > tooltipX) { - tooltipX = offsetX + caretX - tooltipWidth + arrowMinIndent; - } - - arrowX = offsetX + caretX - tooltipX; - } - - // Compute Y position - switch (tooltip.yAlign) { - case "top": - tooltipY = offsetY + caretY + arrowSize + caretPadding; - break; - case "center": - tooltipY = offsetY + caretY - tooltipHeight / 2; - if (tooltip.xAlign === "left") { - tooltipX += arrowSize; - } else if (tooltip.xAlign === "right") { - tooltipX -= arrowSize; - } - - break; - case "bottom": - tooltipY = offsetY + caretY - tooltipHeight - arrowSize - caretPadding; - break; - default: - break; - } - - // Position tooltip and display - tooltipEl.style.top = tooltipY.toFixed(1) + "px"; - tooltipEl.style.left = tooltipX.toFixed(1) + "px"; - if (arrowX === undefined) { - tooltipEl.querySelector(".arrow").style.left = ""; - } else { - // Calculate percentage X value depending on the tooltip's - // width to avoid hanging arrow out on tooltip width changes - var arrowXpercent = ((100 / tooltipWidth) * arrowX).toFixed(1); - tooltipEl.querySelector(".arrow").style.left = arrowXpercent + "%"; - } - - tooltipEl.style.opacity = 1; -}; - // Functions to update data in page var failures = 0; @@ -613,129 +411,6 @@ function updateSummaryData(runOnce) { }); } -function doughnutTooltip(tooltipLabel) { - var percentageTotalShown = tooltipLabel.chart._metasets[0].total.toFixed(1); - // tooltipLabel.chart._metasets[0].total returns the total percentage of the shown slices - // to compensate rounding errors we round to one decimal - - var label = " " + tooltipLabel.label; - var itemPercentage; - - // if we only show < 1% percent of all, show each item with two decimals - if (percentageTotalShown < 1) { - itemPercentage = tooltipLabel.parsed.toFixed(2); - } else { - // show with one decimal, but in case the item share is really small it could be rounded to 0.0 - // we compensate for this - itemPercentage = - tooltipLabel.parsed.toFixed(1) === "0.0" ? "< 0.1" : tooltipLabel.parsed.toFixed(1); - } - - // even if no doughnut slice is hidden, sometimes percentageTotalShown is slightly less then 100 - // we therefore use 99.9 to decide if slices are hidden (we only show with 0.1 precision) - if (percentageTotalShown > 99.9) { - // All items shown - return label + ": " + itemPercentage + "%"; - } else { - // set percentageTotalShown again without rounding to account - // for cases where the total shown percentage would be <0.1% of all - percentageTotalShown = tooltipLabel.chart._metasets[0].total; - return ( - label + - ":
• " + - itemPercentage + - "% of all queries
• " + - ((tooltipLabel.parsed * 100) / percentageTotalShown).toFixed(1) + - "% of shown items" - ); - } -} - -// chartjs plugin used by the custom doughnut legend -const getOrCreateLegendList = (chart, id) => { - const legendContainer = document.getElementById(id); - let listContainer = legendContainer.querySelector("ul"); - - if (!listContainer) { - listContainer = document.createElement("ul"); - listContainer.style.display = "flex"; - listContainer.style.flexDirection = "column"; - listContainer.style.margin = 0; - listContainer.style.padding = 0; - - legendContainer.append(listContainer); - } - - return listContainer; -}; - -const htmlLegendPlugin = { - id: "htmlLegend", - afterUpdate(chart, args, options) { - const ul = getOrCreateLegendList(chart, options.containerID); - - // Remove old legend items - while (ul.firstChild) { - ul.firstChild.remove(); - } - - // Reuse the built-in legendItems generator - const items = chart.options.plugins.legend.labels.generateLabels(chart); - - items.forEach(item => { - const li = document.createElement("li"); - li.style.alignItems = "center"; - li.style.cursor = "pointer"; - li.style.display = "flex"; - li.style.flexDirection = "row"; - - // Color checkbox (toggle visibility) - const boxSpan = document.createElement("span"); - boxSpan.title = "Toggle visibility"; - boxSpan.style.color = item.fillStyle; - boxSpan.style.display = "inline-block"; - boxSpan.style.margin = "0 10px"; - boxSpan.innerHTML = item.hidden - ? '' - : ''; - - boxSpan.addEventListener("click", () => { - const { type } = chart.config; - - if (type === "pie" || type === "doughnut") { - // Pie and doughnut charts only have a single dataset and visibility is per item - chart.toggleDataVisibility(item.index); - } else { - chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); - } - - chart.update(); - }); - - // Text (link to query log page) - const textLink = document.createElement("p"); - textLink.title = "List " + item.text + " queries"; - textLink.style.color = item.fontColor; - textLink.style.margin = 0; - textLink.style.padding = 0; - textLink.style.textDecoration = item.hidden ? "line-through" : ""; - textLink.className = "legend-label-text"; - textLink.append(item.text); - - textLink.addEventListener("click", () => { - if (chart.canvas.id === "queryTypePieChart") { - window.location.href = "queries.lp?querytype=" + querytypeids[item.index]; - } else if (chart.canvas.id === "forwardDestinationPieChart") { - window.location.href = "queries.lp?forwarddest=" + encodeURIComponent(item.text); - } - }); - - li.append(boxSpan, textLink); - ul.append(li); - }); - }, -}; - $(function () { // Pull in data via AJAX updateSummaryData(); diff --git a/scripts/pi-hole/js/settings-system.js b/scripts/pi-hole/js/settings-system.js index a0a7fe49..470089bc 100644 --- a/scripts/pi-hole/js/settings-system.js +++ b/scripts/pi-hole/js/settings-system.js @@ -5,9 +5,51 @@ * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ -/* global apiFailure:false */ +/* global apiFailure:false, Chart:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false */ var hostinfoTimer = null; +var cachePieChart = null; +var cacheSize = 0; + +var querytypeids = []; +function updateCachePie(data) { + var v = [], + c = [], + k = [], + i = 0, + sum = 0; + + // Compute total number of queries + Object.keys(data).forEach(function (item) { + sum += data[item]; + }); + + // Add empty space to chart + data.empty = cacheSize - sum; + sum = cacheSize; + + // Fill chart with data + querytypeids = []; + Object.keys(data).forEach(function (item) { + v.push((100 * data[item]) / sum); + c.push(THEME_COLORS[i % THEME_COLORS.length]); + k.push(item); + querytypeids.push(i + 1); + + i++; + }); + + // Build a single dataset with the data to be pushed + var dd = { data: v, backgroundColor: c }; + // and push it at once + cachePieChart.data.datasets[0] = dd; + cachePieChart.data.labels = k; + $("#cache-pie-chart .overlay").hide(); + cachePieChart.update(); + + // Don't use rotation animation for further updates + cachePieChart.options.animation.duration = 0; +} function updateHostInfo() { $.ajax({ @@ -45,16 +87,22 @@ function updateHostInfo() { // Walk nested objects, create a dash-separated global key and assign the value // to the corresponding element (add percentage for DNS replies) function setMetrics(data, prefix) { + var cacheData = {}; for (const [key, val] of Object.entries(data)) { if (prefix === "sysinfo-dns-cache-content-") { // Create table row for each DNS cache entry - const name = - val.name !== "OTHER" - ? "Valid " + (val.name !== null ? val.name : "TYPE " + val.type) - : "Other valid"; - const tr = "" + name + " records in cache:" + val.count + ""; - // Append row to DNS cache table - $("#dns-cache-table").append(tr); + // (if table exists) + if ($("#dns-cache-table").length > 0) { + const name = + val.name !== "OTHER" + ? "Valid " + (val.name !== null ? val.name : "TYPE " + val.type) + : "Other valid"; + const tr = "" + name + " records in cache:" + val.count + ""; + // Append row to DNS cache table + $("#dns-cache-table").append(tr); + } + + cacheData[val.name] = val.count; } else if (typeof val === "object") { setMetrics(val, prefix + key + "-"); } else if (prefix === "sysinfo-dns-replies-") { @@ -66,6 +114,11 @@ function setMetrics(data, prefix) { $("#" + prefix + key).text(lval); } } + + // Draw pie chart if data is available + if (Object.keys(cacheData).length > 0) { + updateCachePie(cacheData); + } } var metricsTimer = null; @@ -77,6 +130,11 @@ function updateMetrics() { .done(function (data) { var metrics = data.metrics; $("#dns-cache-table").empty(); + + // Set global cache size + cacheSize = metrics.dns.cache.size; + + // Set metrics setMetrics(metrics, "sysinfo-"); $("div[id^='sysinfo-metrics-overlay']").hide(); @@ -212,4 +270,45 @@ $(function () { updateHostInfo(); updateMetrics(); showQueryLoggingButton(); + + var ctx = document.getElementById("cachePieChart").getContext("2d"); + cachePieChart = new Chart(ctx, { + type: "doughnut", + data: { + labels: [], + datasets: [{ data: [], parsing: false }], + }, + plugins: [htmlLegendPlugin], + options: { + responsive: true, + maintainAspectRatio: true, + elements: { + arc: { + borderColor: $(".box").css("background-color"), + }, + }, + plugins: { + htmlLegend: { + containerID: "cache-legend", + }, + legend: { + display: false, + }, + tooltip: { + // Disable the on-canvas tooltip + enabled: false, + external: customTooltips, + callbacks: { + title: function () { + return "Cache content"; + }, + label: doughnutTooltip, + }, + }, + }, + animation: { + duration: 750, + }, + }, + }); }); diff --git a/settings-system.lp b/settings-system.lp index 97d50bdb..023db375 100644 --- a/settings-system.lp +++ b/settings-system.lp @@ -159,7 +159,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
-
+

DNS cache metrics

@@ -200,10 +200,17 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r') - + + +
+
+ +
+
+ See also our DNS cache documentation.
@@ -297,6 +304,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r') +