mirror of
https://github.com/pi-hole/web.git
synced 2025-12-24 12:48:29 +00:00
Add cache content pie chart on settings->system page and move all chart-related code into a shared file pi-hole/js/chart.js
Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
1
index.lp
1
index.lp
@@ -275,6 +275,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<script src="<?=pihole.fileversion('scripts/pi-hole/js/charts.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/pi-hole/js/index.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/pi-hole/lua/footer.lp','r')?>
|
||||
|
||||
343
scripts/pi-hole/js/charts.js
Normal file
343
scripts/pi-hole/js/charts.js
Normal file
@@ -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
|
||||
? '<i class="colorBoxWrapper fa fa-square"></i>'
|
||||
: '<i class="colorBoxWrapper fa fa-check-square"></i>';
|
||||
|
||||
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 = "<div class='arrow'></div> <table></table>";
|
||||
// avoid browser's font-zoom since we know that <body>'s
|
||||
// font-size was set to 14px by bootstrap's css
|
||||
var fontZoom = parseFloat($("body").css("font-size")) / 14;
|
||||
// set styles and font
|
||||
tooltipEl.style.padding = tooltip.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 = "<thead>";
|
||||
|
||||
titleLines.forEach(function (title) {
|
||||
innerHtml += "<tr><th>" + title + "</th></tr>";
|
||||
});
|
||||
innerHtml += "</thead><tbody>";
|
||||
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 = "<span class='chartjs-tooltip-key' style='" + style + "'></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 += "<tr><td>" + span + body + "</td></tr>";
|
||||
printed++;
|
||||
}
|
||||
});
|
||||
if (printed < 1) {
|
||||
innerHtml += "<tr><td>No activity recorded</td></tr>";
|
||||
}
|
||||
|
||||
innerHtml += "</tbody>";
|
||||
|
||||
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 +
|
||||
":<br>• " +
|
||||
itemPercentage +
|
||||
"% of all data<br>• " +
|
||||
((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;
|
||||
};
|
||||
@@ -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 = "<div class='arrow'></div> <table></table>";
|
||||
// avoid browser's font-zoom since we know that <body>'s
|
||||
// font-size was set to 14px by bootstrap's css
|
||||
var fontZoom = parseFloat($("body").css("font-size")) / 14;
|
||||
// set styles and font
|
||||
tooltipEl.style.padding = tooltip.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 = "<thead>";
|
||||
|
||||
titleLines.forEach(function (title) {
|
||||
innerHtml += "<tr><th>" + title + "</th></tr>";
|
||||
});
|
||||
innerHtml += "</thead><tbody>";
|
||||
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 = "<span class='chartjs-tooltip-key' style='" + style + "'></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 += "<tr><td>" + span + body + "</td></tr>";
|
||||
printed++;
|
||||
}
|
||||
});
|
||||
if (printed < 1) {
|
||||
innerHtml += "<tr><td>No activity recorded</td></tr>";
|
||||
}
|
||||
|
||||
innerHtml += "</tbody>";
|
||||
|
||||
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 +
|
||||
":<br>• " +
|
||||
itemPercentage +
|
||||
"% of all queries<br>• " +
|
||||
((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
|
||||
? '<i class="colorBoxWrapper fa fa-square"></i>'
|
||||
: '<i class="colorBoxWrapper fa fa-check-square"></i>';
|
||||
|
||||
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();
|
||||
|
||||
@@ -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,9 +87,12 @@ 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
|
||||
// (if table exists)
|
||||
if ($("#dns-cache-table").length > 0) {
|
||||
const name =
|
||||
val.name !== "OTHER"
|
||||
? "Valid " + (val.name !== null ? val.name : "TYPE " + val.type)
|
||||
@@ -55,6 +100,9 @@ function setMetrics(data, prefix) {
|
||||
const tr = "<tr><th>" + name + " records in cache:</th><td>" + val.count + "</td></tr>";
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,7 +159,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="box">
|
||||
<div class="box settings-level-1" id="cache-metrics">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">DNS cache metrics</h3>
|
||||
</div>
|
||||
@@ -200,10 +200,17 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table table-striped table-bordered nowrap settings-level-2">
|
||||
<!-- <table class="table table-striped table-bordered nowrap settings-level-2">
|
||||
<tbody id="dns-cache-table">
|
||||
</tbody>
|
||||
</table>
|
||||
</table> -->
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
<div style="width:50%">
|
||||
<canvas id="cachePieChart" width="280" height="280"></canvas>
|
||||
</div>
|
||||
<div class="chart-legend" style="width:50%" id="cache-legend" ></div>
|
||||
<!-- /.box-body -->
|
||||
See also our <a href="https://docs.pi-hole.net/ftldns/dns-cache/" rel="noopener" target="_blank">DNS cache documentation</a>.
|
||||
</div>
|
||||
</div>
|
||||
@@ -297,6 +304,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/jquery.confirm.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/pi-hole/js/charts.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/pi-hole/js/settings-system.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/pi-hole/js/settings.js')?>"></script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user