Skip to content

Commit 4405ae1

Browse files
committed
feat: add stats metric filters
1 parent 5d75d67 commit 4405ae1

1 file changed

Lines changed: 79 additions & 6 deletions

File tree

defaults/public/_stats/index.html

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,32 @@
100100
margin: 1.5rem 0;
101101
}
102102
.card {
103+
appearance: none;
103104
border: 1px solid var(--border);
104105
border-radius: 0.5rem;
105106
padding: 1rem;
106107
background: var(--panel);
108+
color: inherit;
109+
cursor: pointer;
110+
font: inherit;
111+
text-align: left;
112+
transition:
113+
border-color 160ms ease,
114+
box-shadow 160ms ease,
115+
transform 160ms ease;
116+
}
117+
.card:hover {
118+
border-color: var(--brand);
119+
box-shadow: 0 0.6rem 1.5rem rgb(15 118 110 / 0.12);
120+
transform: translateY(-1px);
121+
}
122+
.card:focus-visible {
123+
outline: 3px solid color-mix(in srgb, var(--brand) 35%, transparent);
124+
outline-offset: 2px;
125+
}
126+
.card[aria-pressed="true"] {
127+
border-color: var(--brand);
128+
box-shadow: inset 0 0 0 1px var(--brand);
107129
}
108130
.metric {
109131
font-size: 2rem;
@@ -202,6 +224,14 @@
202224
color: var(--brand);
203225
font-weight: 700;
204226
}
227+
.filter-summary {
228+
min-height: 1.2rem;
229+
color: var(--muted);
230+
font-size: 0.9rem;
231+
}
232+
.filter-summary strong {
233+
color: var(--text);
234+
}
205235
</style>
206236
</head>
207237
<body>
@@ -218,6 +248,7 @@ <h1>Dashboard</h1>
218248
<section class="grid" id="metrics"></section>
219249

220250
<input id="search" type="search" placeholder="Search path, target, title, description, owner, tags…" />
251+
<div class="filter-summary" id="filter-summary" aria-live="polite"></div>
221252

222253
<table>
223254
<thead>
@@ -244,10 +275,12 @@ <h1>Dashboard</h1>
244275
const rowsEl = document.getElementById("rows");
245276
const metricsEl = document.getElementById("metrics");
246277
const searchEl = document.getElementById("search");
278+
const filterSummaryEl = document.getElementById("filter-summary");
247279
const generatedEl = document.getElementById("generated");
248280

249281
let allLinks = [];
250282
let registry = null;
283+
const activeStates = new Set();
251284

252285
main().catch((error) => {
253286
rowsEl.innerHTML = `<tr><td colspan="7">Failed to load registry: ${escapeHtml(error.message)}</td></tr>`;
@@ -259,13 +292,29 @@ <h1>Dashboard</h1>
259292
allLinks = flattenRegistry(registry);
260293
generatedEl.innerHTML = renderGeneratedFooter(registry);
261294
renderMetrics(allLinks);
262-
renderRows(allLinks);
295+
applyFilters();
263296
}
264297

265298
searchEl.addEventListener("input", () => {
266-
const q = searchEl.value.trim().toLowerCase();
267-
const filtered = !q ? allLinks : allLinks.filter((link) => JSON.stringify(link).toLowerCase().includes(q));
268-
renderRows(filtered);
299+
applyFilters();
300+
});
301+
302+
metricsEl.addEventListener("click", (event) => {
303+
const button = event.target.closest("button[data-filter-state]");
304+
if (!button) return;
305+
306+
const state = button.dataset.filterState;
307+
if (state === "total") {
308+
activeStates.clear();
309+
searchEl.value = "";
310+
} else if (activeStates.has(state)) {
311+
activeStates.delete(state);
312+
} else {
313+
activeStates.add(state);
314+
}
315+
316+
renderMetrics(allLinks);
317+
applyFilters();
269318
});
270319

271320
function flattenRegistry(registry) {
@@ -330,15 +379,39 @@ <h1>Dashboard</h1>
330379
metricsEl.innerHTML = items
331380
.map(
332381
(key) => `
333-
<div class="card">
382+
<button class="card" type="button" data-filter-state="${escapeAttr(key)}" aria-pressed="${key !== "total" && activeStates.has(key) ? "true" : "false"}" title="${key === "total" ? "Clear filters" : `Filter by ${label(key)}`}">
334383
<div class="metric">${counts[key] || 0}</div>
335384
<div class="muted">${label(key)}</div>
336-
</div>
385+
</button>
337386
`
338387
)
339388
.join("");
340389
}
341390

391+
function applyFilters() {
392+
const q = searchEl.value.trim().toLowerCase();
393+
const filtered = allLinks.filter((link) => {
394+
if (activeStates.size && !activeStates.has(effectiveState(link))) return false;
395+
if (!q) return true;
396+
return JSON.stringify(link).toLowerCase().includes(q);
397+
});
398+
399+
renderRows(filtered);
400+
renderFilterSummary(filtered.length, q);
401+
}
402+
403+
function renderFilterSummary(count, query) {
404+
const filters = [];
405+
if (query) filters.push(`search <strong>${escapeHtml(query)}</strong>`);
406+
if (activeStates.size) {
407+
filters.push(`state <strong>${[...activeStates].map(label).join(" or ")}</strong>`);
408+
}
409+
410+
filterSummaryEl.innerHTML = filters.length
411+
? `${count} matching route${count === 1 ? "" : "s"} where ${filters.join(" and ")}`
412+
: "";
413+
}
414+
342415
function renderRows(links) {
343416
if (!links.length) {
344417
rowsEl.innerHTML = `<tr><td colspan="7" class="muted">No matching routes.</td></tr>`;

0 commit comments

Comments
 (0)