merge newui branch
This commit is contained in:
@@ -0,0 +1,482 @@
|
||||
function getChartHeight() {
|
||||
return document.body.clientWidth > 600 && document.body.clientHeight >= 720 ? 150 : 100;
|
||||
}
|
||||
|
||||
export function logsChart(logsSettings) {
|
||||
const data = store({
|
||||
stats: [],
|
||||
});
|
||||
|
||||
async function loadAndInit(el) {
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
logsSettings.isChartLoading = true;
|
||||
|
||||
try {
|
||||
const normalizedFilter = (logsSettings.presets || []).concat(
|
||||
app.utils.normalizeSearchFilter(logsSettings.filter, ["level", "message", "data"]),
|
||||
);
|
||||
|
||||
const stats = await app.pb.logs.getStats({
|
||||
filter: normalizedFilter
|
||||
.filter(Boolean)
|
||||
.map((f) => "(" + f + ")")
|
||||
.join("&&"),
|
||||
});
|
||||
|
||||
const totalStats = stats.length;
|
||||
|
||||
const timestamps = [];
|
||||
const totals = [];
|
||||
|
||||
logsSettings.totalFound = 0;
|
||||
|
||||
for (let i = 0; i < totalStats; i++) {
|
||||
const unix = new Date(stats[i].date.replace(" ", "T")).getTime() / 1000;
|
||||
timestamps.push(unix);
|
||||
totals.push(stats[i].total);
|
||||
|
||||
logsSettings.totalFound += stats[i].total;
|
||||
|
||||
// if between the current and next point there is more than 1h difference
|
||||
// insert a 0 point for a flat 1h line in the stepped chart visualization
|
||||
if (stats[i + 1]?.date) {
|
||||
const unixNext = new Date(stats[i + 1].date.replace(" ", "T")).getTime() / 1000;
|
||||
if (unixNext - unix > 3600) {
|
||||
timestamps.push(unix + 3600);
|
||||
totals.push(0);
|
||||
}
|
||||
} else if (i + 1 == totalStats) {
|
||||
timestamps.push(unix + 3600);
|
||||
totals.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
data.stats = stats;
|
||||
logsSettings.isChartLoading = false;
|
||||
|
||||
initChart(el, [timestamps, totals], logsSettings);
|
||||
} catch (err) {
|
||||
if (!err?.isAbort) {
|
||||
logsSettings.isChartLoading = false;
|
||||
// only log to avoid showing multiple errors with the logs listing
|
||||
// app.checkApiError(err)
|
||||
console.warn("failed to load logs chart:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const watchers = [];
|
||||
|
||||
return t.div(
|
||||
{
|
||||
pbEvent: "logsChart",
|
||||
className: () =>
|
||||
[
|
||||
"logs-chart",
|
||||
logsSettings.isChartLoading ? "loading" : null,
|
||||
logsSettings.zoom?.min && logsSettings.zoom?.max ? "zoomed" : "",
|
||||
!data.stats.length || !logsSettings.isFirstLoadReady ? "nodata" : null,
|
||||
].filter(Boolean).join(" "),
|
||||
onmount: (el) => {
|
||||
// init and refresh chart
|
||||
watchers.push(
|
||||
watch(
|
||||
() => [logsSettings.reset, logsSettings.filter, logsSettings.presets?.length],
|
||||
() => loadAndInit(el),
|
||||
),
|
||||
);
|
||||
|
||||
el._resizeChartFunc = () => {
|
||||
clearTimeout(el._resizeTimeoutId);
|
||||
el._resizeTimeoutId = setTimeout(() => {
|
||||
if (!el?._uplot) {
|
||||
return;
|
||||
}
|
||||
|
||||
el._uplot.setSize({
|
||||
width: el.clientWidth,
|
||||
height: getChartHeight(),
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
window.addEventListener("resize", el._resizeChartFunc);
|
||||
},
|
||||
onunmount: (el) => {
|
||||
watchers.forEach((w) => w?.unwatch());
|
||||
|
||||
el._uplot?.destroy();
|
||||
|
||||
if (el._resizeChartFunc) {
|
||||
clearTimeout(el._resizeTimeoutId);
|
||||
window.removeEventListener("resize", el._resizeChartFunc);
|
||||
el._resizeChartFunc = null;
|
||||
el._resizeTimeoutId = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: () =>
|
||||
`logs-reset-zoom-ctrl ${logsSettings.zoom?.min && logsSettings.zoom?.max ? "" : "hidden"}`,
|
||||
onclick() {
|
||||
logsSettings.zoom = {};
|
||||
},
|
||||
},
|
||||
t.div({ className: "content-primary" }, "Reset zoom"),
|
||||
t.div({ className: "content-secondary" }, "(drag the timeline to pan)"),
|
||||
),
|
||||
t.span({
|
||||
hidden: () => !logsSettings.isChartLoading,
|
||||
className: () => "loader logs-chart-loader",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function resetChartZoom(chart) {
|
||||
const data = chart?.data?.[0];
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
chart.setScale("x", {
|
||||
min: data[0],
|
||||
max: data[data.length - 1],
|
||||
});
|
||||
}
|
||||
|
||||
function initChart(el, dataPoints, logsSettings) {
|
||||
const computedStyle = window.getComputedStyle(el);
|
||||
const gridTxtColor = computedStyle.getPropertyValue("--surfaceTxtHintColor");
|
||||
const gridColor = computedStyle.getPropertyValue("--surfaceAlt1Color");
|
||||
const strokeColor = computedStyle.getPropertyValue("--surfaceAlt4Color");
|
||||
const fillColor = computedStyle.getPropertyValue("--surfaceAlt2Color");
|
||||
|
||||
const opts = {
|
||||
width: el.clientWidth,
|
||||
height: getChartHeight(),
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
cursor: {
|
||||
x: false,
|
||||
y: false,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
range: (self, newMin, newMax) => {
|
||||
// disallow pan beyond the dataPoints edges
|
||||
if (newMin < dataPoints[0][0] || newMax > dataPoints[0][dataPoints[0].length - 1]) {
|
||||
return [self.scales.x.min, self.scales.x.max];
|
||||
}
|
||||
|
||||
// allow zoom
|
||||
return [newMin, newMax];
|
||||
},
|
||||
},
|
||||
y: {
|
||||
range: {
|
||||
min: {
|
||||
pad: 0.2,
|
||||
soft: 0,
|
||||
mode: 1,
|
||||
},
|
||||
max: {
|
||||
pad: 0.2,
|
||||
soft: 0,
|
||||
mode: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{},
|
||||
{
|
||||
paths: uPlot.paths.stepped({ align: 1 }),
|
||||
points: {
|
||||
show: false,
|
||||
size: 1,
|
||||
},
|
||||
width: 2,
|
||||
fill: fillColor,
|
||||
stroke: strokeColor,
|
||||
},
|
||||
],
|
||||
axes: [
|
||||
{
|
||||
show: true,
|
||||
size: 35,
|
||||
lineGap: 1,
|
||||
space: 60,
|
||||
stroke: gridTxtColor,
|
||||
incrs: [
|
||||
// minutes
|
||||
60 * 5,
|
||||
60 * 10,
|
||||
60 * 15,
|
||||
60 * 30,
|
||||
// hours
|
||||
3600,
|
||||
3600 * 2,
|
||||
3600 * 3,
|
||||
3600 * 4,
|
||||
3600 * 6,
|
||||
3600 * 12,
|
||||
// days
|
||||
86400,
|
||||
86400 * 2,
|
||||
],
|
||||
// dprint-ignore
|
||||
values: [
|
||||
// incr default year month day hour min sec mode
|
||||
[3600, "{h}{aa}", "\n{MMM} {DD}", null, "\n{MMM} {DD}", null, null, null, 1],
|
||||
[60, "{h}:{mm}{aa}", "\n{MMM} {DD}", null, "\n{MMM} {DD}", null, null, null, 1],
|
||||
],
|
||||
grid: {
|
||||
show: true,
|
||||
stroke: gridColor,
|
||||
width: 1,
|
||||
},
|
||||
ticks: {
|
||||
show: true,
|
||||
stroke: gridColor,
|
||||
width: 1,
|
||||
size: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
stroke: gridTxtColor,
|
||||
grid: {
|
||||
show: true,
|
||||
stroke: gridColor,
|
||||
width: 1,
|
||||
},
|
||||
ticks: {
|
||||
show: true,
|
||||
stroke: gridColor,
|
||||
width: 1,
|
||||
size: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [tooltipsPlugin(), zoomSetPlugin(logsSettings), xPanPlugin(logsSettings)],
|
||||
};
|
||||
|
||||
el._uplot?.destroy();
|
||||
el._uplot = new uPlot(opts, dataPoints, el);
|
||||
}
|
||||
|
||||
// based on view-source:https://leeoniya.github.io/uPlot/demos/zoom-fetch.html
|
||||
function zoomSetPlugin(logsSettings) {
|
||||
let zoomWatcher;
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
init: (u) => {
|
||||
u.over.ondblclick = (e) => {
|
||||
logsSettings.zoom = {};
|
||||
};
|
||||
|
||||
zoomWatcher = watch(() => {
|
||||
if (!logsSettings.zoom?.min || !logsSettings.zoom?.max) {
|
||||
resetChartZoom(u);
|
||||
} else {
|
||||
u.setScale("x", {
|
||||
min: logsSettings.zoom.min,
|
||||
max: logsSettings.zoom.max,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
destroy: (u) => {
|
||||
zoomWatcher?.unwatch();
|
||||
},
|
||||
setSelect: (u) => {
|
||||
if (u.select.width > 0) {
|
||||
logsSettings.zoom = {
|
||||
min: u.posToVal(u.select.left, "x"),
|
||||
max: u.posToVal(u.select.left + u.select.width, "x"),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// based on https://leeoniya.github.io/uPlot/demos/y-scale-drag.html
|
||||
function xPanPlugin(logsSettings) {
|
||||
let debounceTimeout;
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
init: (u) => {
|
||||
let axisElems = u.root.querySelectorAll(".u-axis");
|
||||
if (!axisElems.length) {
|
||||
console.warn("xPanPlugin requires x axis to be defined");
|
||||
return;
|
||||
}
|
||||
|
||||
axisElems[0].addEventListener("mousedown", (e) => {
|
||||
if (!logsSettings.zoom?.min) {
|
||||
return; // no zoom
|
||||
}
|
||||
|
||||
let x0 = e.clientX;
|
||||
let scale = u.scales.x;
|
||||
let { min, max } = scale;
|
||||
let dim = u.bbox.width;
|
||||
let unitsPerPx = (max - min) / (dim / uPlot.pxRatio);
|
||||
|
||||
const mousemoveFunc = (e) => {
|
||||
let diff = x0 - e.clientX;
|
||||
let shiftBy = diff * unitsPerPx;
|
||||
|
||||
u.setScale("x", {
|
||||
min: min + shiftBy,
|
||||
max: max + shiftBy,
|
||||
});
|
||||
|
||||
clearTimeout(debounceTimeout);
|
||||
debounceTimeout = setTimeout(() => {
|
||||
if (u?.scales?.x) {
|
||||
logsSettings.zoom = {
|
||||
min: u.scales.x.min,
|
||||
max: u.scales.x.max,
|
||||
};
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const mouseupFunc = (e) => {
|
||||
document.removeEventListener("mousemove", mousemoveFunc);
|
||||
document.removeEventListener("mouseup", mouseupFunc);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", mousemoveFunc);
|
||||
document.addEventListener("mouseup", mouseupFunc);
|
||||
});
|
||||
},
|
||||
destroy: (u) => {
|
||||
if (debounceTimeout) {
|
||||
clearTimeout(debounceTimeout);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// based on https://leeoniya.github.io/uPlot/demos/tooltips.html
|
||||
function tooltipsPlugin(logsSettings) {
|
||||
let tooltip;
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
init: (u) => {
|
||||
const over = u.over;
|
||||
|
||||
tooltip = store({
|
||||
date: "",
|
||||
total: 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
show: false,
|
||||
});
|
||||
|
||||
const tooltipEl = t.div(
|
||||
{
|
||||
className: () => `chart-tooltip ${tooltip.show ? "" : "hidden"}`,
|
||||
onmount(el) {
|
||||
el._positionWatcher?.unwatch();
|
||||
el._positionWatcher = watch(
|
||||
() => [tooltip.left, tooltip.top],
|
||||
() => {
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
let left = tooltip.left;
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
} else if (left + rect.width > over.clientWidth) {
|
||||
left = over.clientWidth - rect.width;
|
||||
}
|
||||
el.style.left = left + "px";
|
||||
|
||||
const vOffset = 5;
|
||||
let top = tooltip.top - rect.height - vOffset;
|
||||
if (top < 0) {
|
||||
top = tooltip.top + vOffset;
|
||||
if (top + rect.high > over.clientHeight) {
|
||||
top = over.clientHeight - rect.height;
|
||||
}
|
||||
}
|
||||
el.style.top = top + "px";
|
||||
},
|
||||
);
|
||||
},
|
||||
onunmount(el) {
|
||||
el._positionWatcher?.unwatch();
|
||||
},
|
||||
},
|
||||
t.div(
|
||||
{ className: "content-primary" },
|
||||
() => `${tooltip.total} ${tooltip.total == 1 ? "request" : "requests"}`,
|
||||
),
|
||||
t.div({ className: "content-secondary" }, () => tooltip.date),
|
||||
);
|
||||
over.appendChild(tooltipEl);
|
||||
|
||||
over.addEventListener("mouseleave", () => {
|
||||
if (tooltip) {
|
||||
tooltip.show = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
destroy: () => {
|
||||
tooltip.show = false;
|
||||
},
|
||||
setCursor: (u) => {
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
const xVal = u.data[0][u.cursor.idx] || 0;
|
||||
const yVal = u.data[1][u.cursor.idx] || 0;
|
||||
|
||||
// skip zero points
|
||||
if (xVal == 0 || yVal == 0) {
|
||||
tooltip.show = false;
|
||||
return;
|
||||
}
|
||||
|
||||
tooltip.show = true;
|
||||
tooltip.total = yVal;
|
||||
|
||||
const xDateStart = new Date(xVal * 1000);
|
||||
const xDateEnd = new Date(xVal * 1000 + 3600000); // all stats are hourly based
|
||||
const monthName = xDateStart.toLocaleString("default", { month: "short" });
|
||||
const day = xDateStart.getDate().toString().padStart(2, "0");
|
||||
tooltip.date = `${monthName} ${day} ${dateHour(xDateStart)}-${dateHour(xDateEnd)}`;
|
||||
|
||||
tooltip.left = Math.round(u.valToPos(xVal, "x"));
|
||||
tooltip.top = Math.round(u.valToPos(yVal, "y"));
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function dateHour(date) {
|
||||
let hours = date.getHours();
|
||||
let ampm = hours >= 12 ? "pm" : "am";
|
||||
|
||||
// normalize
|
||||
hours = hours % 12 || 12;
|
||||
|
||||
return hours + ampm;
|
||||
}
|
||||
Reference in New Issue
Block a user