feat: add tooltip to status page
This commit is contained in:
parent
3a4fa6c0fe
commit
0e009dc684
|
|
@ -1,5 +1,8 @@
|
||||||
// Click-to-expand for compact display mode on the public status page.
|
// Public status page client JS:
|
||||||
// Loaded by status pages that have at least one monitor in compact mode.
|
// 1. Bar tooltips (always active) — hover any heartbeat bar to see the
|
||||||
|
// bucket time range and uptime breakdown.
|
||||||
|
// 2. Click-to-expand for compact display mode (only fires when there are
|
||||||
|
// compact monitors on the page).
|
||||||
// Reads its config from window.PINGQL_PAGE.
|
// Reads its config from window.PINGQL_PAGE.
|
||||||
(function () {
|
(function () {
|
||||||
var cfg = window.PINGQL_PAGE || {};
|
var cfg = window.PINGQL_PAGE || {};
|
||||||
|
|
@ -8,6 +11,81 @@
|
||||||
var showResponseTime = !!cfg.show_response_time;
|
var showResponseTime = !!cfg.show_response_time;
|
||||||
if (!slug) return;
|
if (!slug) return;
|
||||||
|
|
||||||
|
// ── Bar tooltips ──────────────────────────────────────────────────────
|
||||||
|
var tooltip = document.getElementById("bar-tooltip");
|
||||||
|
if (tooltip) {
|
||||||
|
var bucketSpanMs = (defaultWindow === "24h") ? 3600 * 1000
|
||||||
|
: (defaultWindow === "7d") ? 86400 * 1000
|
||||||
|
: (defaultWindow === "30d") ? 86400 * 1000
|
||||||
|
: 7 * 86400 * 1000;
|
||||||
|
function fmtBucketRange(startIso) {
|
||||||
|
var start = new Date(startIso);
|
||||||
|
var end = new Date(start.getTime() + bucketSpanMs);
|
||||||
|
var sameDay = start.toDateString() === new Date(end.getTime() - 1).toDateString();
|
||||||
|
var dateOpts = { month: "short", day: "numeric" };
|
||||||
|
var timeOpts = { hour: "2-digit", minute: "2-digit" };
|
||||||
|
if (defaultWindow === "24h") {
|
||||||
|
return start.toLocaleDateString(undefined, dateOpts) + ", " +
|
||||||
|
start.toLocaleTimeString(undefined, timeOpts) + " — " +
|
||||||
|
end.toLocaleTimeString(undefined, timeOpts);
|
||||||
|
}
|
||||||
|
if (defaultWindow === "7d" || defaultWindow === "30d") {
|
||||||
|
return start.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
|
||||||
|
}
|
||||||
|
// 90d → weekly buckets
|
||||||
|
var endLabel = new Date(end.getTime() - 1).toLocaleDateString(undefined, dateOpts);
|
||||||
|
return "Week of " + start.toLocaleDateString(undefined, dateOpts) + " — " + endLabel;
|
||||||
|
}
|
||||||
|
function uptimeBand(p) {
|
||||||
|
if (p == null) return "";
|
||||||
|
if (p >= 99.9) return "good";
|
||||||
|
if (p >= 99.0) return "warn";
|
||||||
|
return "bad";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTooltipForBar(bar, evt) {
|
||||||
|
var total = parseInt(bar.getAttribute("data-total") || "0", 10);
|
||||||
|
var up = parseInt(bar.getAttribute("data-up") || "0", 10);
|
||||||
|
var start = bar.getAttribute("data-start");
|
||||||
|
if (!start) return;
|
||||||
|
var pct = total > 0 ? Math.round(1000 * up / total) / 10 : null;
|
||||||
|
var pctText = pct == null ? "—" : (pct === 100 ? "100%" : pct.toFixed(1) + "%");
|
||||||
|
var html = '<div class="head">' + fmtBucketRange(start) + "</div>";
|
||||||
|
if (total > 0) {
|
||||||
|
html += '<div class="row"><span>Checks</span><span>' + total + "</span></div>";
|
||||||
|
html += '<div class="row"><span>Successful</span><span>' + up + "</span></div>";
|
||||||
|
html += '<div class="row"><span>Uptime</span><span class="pct ' + uptimeBand(pct) + '">' + pctText + "</span></div>";
|
||||||
|
} else {
|
||||||
|
html += '<div class="row"><span>No data</span><span>—</span></div>';
|
||||||
|
}
|
||||||
|
tooltip.innerHTML = html;
|
||||||
|
tooltip.style.display = "block";
|
||||||
|
var rect = bar.getBoundingClientRect();
|
||||||
|
// Position at top-center of the bar.
|
||||||
|
tooltip.style.left = (rect.left + rect.width / 2) + "px";
|
||||||
|
tooltip.style.top = rect.top + "px";
|
||||||
|
}
|
||||||
|
function hideTooltip() {
|
||||||
|
tooltip.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate from each .bars container so dynamically-rendered bars
|
||||||
|
// (compact mode click-to-expand) work too.
|
||||||
|
document.addEventListener("mouseover", function (e) {
|
||||||
|
var bar = e.target && e.target.closest && e.target.closest(".bar");
|
||||||
|
if (bar && bar.parentElement && bar.parentElement.classList.contains("bars")) {
|
||||||
|
showTooltipForBar(bar, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseout", function (e) {
|
||||||
|
var bar = e.target && e.target.closest && e.target.closest(".bar");
|
||||||
|
if (bar) hideTooltip();
|
||||||
|
});
|
||||||
|
// Hide on scroll so the tooltip doesn't lag behind the bar.
|
||||||
|
window.addEventListener("scroll", hideTooltip, { passive: true });
|
||||||
|
}
|
||||||
|
// ── End bar tooltips ──────────────────────────────────────────────────
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
return String(s)
|
return String(s)
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
|
|
|
||||||
|
|
@ -117,10 +117,18 @@
|
||||||
.monitor-name .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.monitor-name .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.monitor-meta { display: flex; gap: 1rem; align-items: center; font-size: 0.85rem; color: var(--muted); }
|
.monitor-meta { display: flex; gap: 1rem; align-items: center; font-size: 0.85rem; color: var(--muted); }
|
||||||
.uptime-pct { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--fg); }
|
.uptime-pct { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--fg); }
|
||||||
.bars { display: flex; gap: 0.1rem; height: 32px; margin-top: 0.75rem; align-items: stretch; }
|
.bars { display: flex; gap: 0.1rem; height: 32px; margin-top: 0.75rem; align-items: stretch; position: relative; }
|
||||||
.bar { flex: 1; min-width: 0; border-radius: 2px; transition: opacity 0.15s; }
|
.bar { flex: 1; min-width: 0; border-radius: 2px; transition: opacity 0.15s; }
|
||||||
.bar:hover { opacity: 0.8; }
|
.bar:hover { opacity: 0.8; }
|
||||||
.bars-meta { display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--muted); margin-top: 0.4rem; }
|
.bars-meta { display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--muted); margin-top: 0.4rem; }
|
||||||
|
/* Tooltip shown when hovering a bar. Single floating element per page, positioned by JS. */
|
||||||
|
#bar-tooltip { position: fixed; pointer-events: none; background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.75rem; font-size: 0.75rem; color: var(--fg); box-shadow: 0 8px 24px rgba(0,0,0,0.3); z-index: 100; min-width: 160px; transform: translate(-50%, -100%); margin-top: -8px; display: none; }
|
||||||
|
#bar-tooltip .head { color: var(--muted); margin-bottom: 0.25rem; font-size: 0.7rem; }
|
||||||
|
#bar-tooltip .row { display: flex; justify-content: space-between; gap: 1rem; }
|
||||||
|
#bar-tooltip .row span:last-child { color: var(--fg); font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||||
|
#bar-tooltip .pct.good { color: var(--bar-up); }
|
||||||
|
#bar-tooltip .pct.warn { color: var(--bar-partial); }
|
||||||
|
#bar-tooltip .pct.bad { color: var(--bar-down); }
|
||||||
/* Multi-window uptime row: four labelled cells side by side. */
|
/* Multi-window uptime row: four labelled cells side by side. */
|
||||||
.uptime-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.5rem; margin-top: 0.75rem; }
|
.uptime-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.5rem; margin-top: 0.75rem; }
|
||||||
.uptime-cell { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.65rem; }
|
.uptime-cell { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.65rem; }
|
||||||
|
|
@ -283,9 +291,9 @@
|
||||||
<span class="uptime-pct"><%= fmtUptime(u[page.default_window === '24h' ? 'd24' : page.default_window === '7d' ? 'd7' : page.default_window === '30d' ? 'd30' : 'd90']) %></span>
|
<span class="uptime-pct"><%= fmtUptime(u[page.default_window === '24h' ? 'd24' : page.default_window === '7d' ? 'd7' : page.default_window === '30d' ? 'd30' : 'd90']) %></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bars" title="<%= statusLabel(m.current_state) %>">
|
<div class="bars" aria-label="<%= statusLabel(m.current_state) %>">
|
||||||
<% buckets.forEach(function(b) { %>
|
<% buckets.forEach(function(b) { %>
|
||||||
<div class="bar" style="background: <%= bucketColor(b) %>;" title="<%= b.total > 0 ? (Math.round(100 * b.up / b.total) + '% up') : 'no data' %>"></div>
|
<div class="bar" style="background: <%= bucketColor(b) %>;" data-start="<%= b.start %>" data-total="<%= b.total %>" data-up="<%= b.up %>"></div>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</div>
|
</div>
|
||||||
<div class="bars-meta">
|
<div class="bars-meta">
|
||||||
|
|
@ -339,10 +347,9 @@
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<% if (anyCompact) { %>
|
<div id="bar-tooltip" role="tooltip"></div>
|
||||||
<script>window.PINGQL_PAGE = <%~ JSON.stringify({ slug: page.slug, default_window: page.default_window, show_response_time: !!page.show_response_time }) %>;</script>
|
<script>window.PINGQL_PAGE = <%~ JSON.stringify({ slug: page.slug, default_window: page.default_window, show_response_time: !!page.show_response_time }) %>;</script>
|
||||||
<script src="/_static/expand.js?v=<%= it.expandJsHash %>" defer></script>
|
<script src="/_static/expand.js?v=<%= it.expandJsHash %>" defer></script>
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% if (page.auto_refresh_s > 0) { %>
|
<% if (page.auto_refresh_s > 0) { %>
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue