Skip to content

Commit 0f9c684

Browse files
feat(ui): improve request dashboard and filtering experience
Improves the DebugProbe index page with better request visibility, filtering, and metrics. Focuses on lightweight observability improvements while keeping DebugProbe simple and developer-focused.
2 parents 28b9ee0 + 390e97b commit 0f9c684

5 files changed

Lines changed: 316 additions & 37 deletions

File tree

DebugProbe.AspNetCore/Assets/css/debugprobe.css

Lines changed: 147 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ a {
2020
}
2121

2222
h2 {
23-
margin-bottom: 10px;
23+
margin: 0 0 4px;
2424
}
2525

2626
h3 {
@@ -41,6 +41,7 @@ h4 {
4141
}
4242

4343
.toolbar,
44+
.index-header,
4445
.topbar,
4546
.accordion-header,
4647
.accordion-meta,
@@ -51,12 +52,81 @@ h4 {
5152
}
5253

5354
.toolbar,
55+
.index-header,
5456
.topbar,
5557
.accordion-header,
5658
.details-item {
5759
justify-content: space-between;
5860
}
5961

62+
.index-header {
63+
gap: 16px;
64+
margin-bottom: 14px;
65+
}
66+
67+
.muted {
68+
margin: 0;
69+
color: #666;
70+
font-size: 13px;
71+
}
72+
73+
.filters {
74+
display: grid;
75+
grid-template-columns: minmax(220px, 1fr) minmax(130px, 170px) minmax(130px, 170px) auto auto;
76+
gap: 10px;
77+
margin-bottom: 14px;
78+
}
79+
80+
.stats-bar {
81+
display: grid;
82+
grid-template-columns: repeat(4, minmax(150px, 1fr));
83+
gap: 10px;
84+
margin-bottom: 14px;
85+
}
86+
87+
.stat-tile {
88+
display: flex;
89+
align-items: center;
90+
justify-content: center;
91+
gap: 8px;
92+
min-height: 52px;
93+
padding: 0 16px;
94+
background: #fff;
95+
border: 1px solid #e9e9e9;
96+
border-radius: 8px;
97+
text-align: center;
98+
}
99+
100+
.stat-tile strong {
101+
color: #1f2937;
102+
font-size: 22px;
103+
line-height: 1;
104+
}
105+
106+
.stat-tile span {
107+
color: #666;
108+
font-size: 13px;
109+
font-weight: 700;
110+
text-transform: uppercase;
111+
}
112+
113+
.filters input,
114+
.filters select {
115+
min-height: 36px;
116+
padding: 0 10px;
117+
background: #fff;
118+
border: 1px solid #ddd;
119+
border-radius: 4px;
120+
color: #222;
121+
font: inherit;
122+
}
123+
124+
.filters input:focus,
125+
.filters select:focus {
126+
outline: 2px solid rgba(108, 92, 231, 0.18);
127+
border-color: #6c5ce7;
128+
}
129+
60130
.topbar {
61131
padding: 18px;
62132
background: #1b1b1b;
@@ -140,6 +210,13 @@ table {
140210
border-collapse: collapse;
141211
}
142212

213+
.table-wrap {
214+
overflow-x: auto;
215+
background: #fff;
216+
border: 1px solid #e9e9e9;
217+
border-radius: 8px;
218+
}
219+
143220
th,
144221
td {
145222
padding: 10px;
@@ -149,16 +226,43 @@ td {
149226

150227
th {
151228
background: #fafafa;
229+
color: #555;
230+
font-size: 12px;
231+
font-weight: 700;
232+
text-transform: uppercase;
152233
}
153234

154235
tr:hover {
155236
background: #f1f1f1;
156237
}
157238

239+
tbody tr:last-child td {
240+
border-bottom: none;
241+
}
242+
158243
.clickable-row {
159244
cursor: pointer;
160245
}
161246

247+
.method-pill {
248+
display: inline-flex;
249+
min-width: 54px;
250+
justify-content: center;
251+
padding: 4px 8px;
252+
background: #f3f4f6;
253+
border-radius: 999px;
254+
color: #333;
255+
font-size: 12px;
256+
font-weight: 700;
257+
}
258+
259+
.empty-state,
260+
.empty-row td {
261+
padding: 24px;
262+
color: #777;
263+
text-align: center;
264+
}
265+
162266
/* =========================
163267
Section Titles
164268
========================= */
@@ -245,6 +349,19 @@ tr:hover {
245349
padding: 14px;
246350
}
247351

352+
.index-header {
353+
align-items: flex-start;
354+
flex-direction: column;
355+
}
356+
357+
.filters {
358+
grid-template-columns: 1fr;
359+
}
360+
361+
.stats-bar {
362+
grid-template-columns: repeat(2, minmax(0, 1fr));
363+
}
364+
248365
.payload-group {
249366
padding: 14px;
250367
}
@@ -373,13 +490,14 @@ pre {
373490

374491
.copy-btn,
375492
.btn-clear,
493+
.btn-secondary,
376494
.trace-id-button {
377495
border-radius: 4px;
378496
cursor: pointer;
379497
}
380498

381499
.copy-btn,
382-
.btn-clear {
500+
.btn-secondary {
383501
background: #2d2d2d;
384502
border: 1px solid #444;
385503
color: #ccc;
@@ -394,16 +512,41 @@ pre {
394512
}
395513

396514
.copy-btn:hover,
397-
.btn-clear:hover {
515+
.btn-secondary:hover {
398516
background: #3a3a3a;
399517
}
400518

401-
.btn-clear {
519+
.btn-secondary {
402520
padding: 6px 12px;
403521
color: #fff;
404522
font-size: 13px;
405523
}
406524

525+
.btn-secondary {
526+
display: inline-flex;
527+
align-items: center;
528+
justify-content: center;
529+
gap: 6px;
530+
min-height: 36px;
531+
background: #fff;
532+
border-color: #ddd;
533+
color: #333;
534+
}
535+
536+
.btn-secondary:hover {
537+
background: #f3f3f3;
538+
}
539+
540+
.btn-clear svg {
541+
width: 15px;
542+
height: 15px;
543+
stroke: currentColor;
544+
stroke-width: 2;
545+
stroke-linecap: round;
546+
stroke-linejoin: round;
547+
fill: none;
548+
}
549+
407550
.trace-id-button {
408551
padding: 6px 10px;
409552
background: #f3f4f6;
Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,72 @@
1-
<div class="container">
2-
<div class="toolbar">
3-
<div></div>
4-
<div class="toolbar-right">
5-
<button id="clearBtn" class="btn-clear">Clear</button>
1+
<div class="container">
2+
<div class="index-header">
3+
<div>
4+
<h2>Requests</h2>
5+
<p class="muted">Showing <span id="visibleCount">{{total_count}}</span> of {{total_count}}</p>
66
</div>
77
</div>
8-
<table>
9-
<thead>
10-
<tr>
11-
<th>Time</th>
12-
<th>Method</th>
13-
<th>Path</th>
14-
<th>Status</th>
15-
</tr>
16-
</thead>
17-
<tbody>
18-
{{rows}}
19-
</tbody>
20-
</table>
21-
</div>
8+
9+
<div class="stats-bar" aria-label="Request summary">
10+
<div class="stat-tile">
11+
<strong>{{total_requests}}</strong>
12+
<span>Requests</span>
13+
</div>
14+
<div class="stat-tile">
15+
<strong>{{avg_response_time}}</strong>
16+
<span>Avg</span>
17+
</div>
18+
<div class="stat-tile">
19+
<strong>{{slow_requests}}</strong>
20+
<span>Over 1s</span>
21+
</div>
22+
<div class="stat-tile">
23+
<strong>{{error_rate}}</strong>
24+
<span>Errors</span>
25+
</div>
26+
</div>
27+
28+
<div class="filters" aria-label="Request filters">
29+
<input id="requestSearch" type="search" placeholder="Search path, method, status, or trace id" autocomplete="off" />
30+
<select id="methodFilter" aria-label="Filter by method">
31+
<option value="">All methods</option>
32+
{{method_options}}
33+
</select>
34+
<select id="statusFilter" aria-label="Filter by status">
35+
<option value="">All statuses</option>
36+
<option value="2">2xx</option>
37+
<option value="3">3xx</option>
38+
<option value="4">4xx</option>
39+
<option value="5">5xx</option>
40+
</select>
41+
<button id="resetFiltersBtn" class="btn-secondary" type="button">Reset</button>
42+
<button id="clearBtn" class="btn-secondary btn-clear" type="button" title="Clear requests" aria-label="Clear requests">
43+
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
44+
<path d="M3 6h18" />
45+
<path d="M8 6V4h8v2" />
46+
<path d="M6 6l1 14h10l1-14" />
47+
<path d="M10 11v5" />
48+
<path d="M14 11v5" />
49+
</svg>
50+
<span>Clear</span>
51+
</button>
52+
</div>
53+
54+
<div class="table-wrap">
55+
<table id="requestTable">
56+
<thead>
57+
<tr>
58+
<th>Time</th>
59+
<th>Method</th>
60+
<th>Path</th>
61+
<th>Status</th>
62+
<th>Duration</th>
63+
</tr>
64+
</thead>
65+
<tbody>
66+
{{rows}}
67+
</tbody>
68+
</table>
69+
</div>
70+
71+
<div id="emptyFilterState" class="empty-state" hidden>No matching requests</div>
72+
</div>

DebugProbe.AspNetCore/Assets/js/debugprobe-ui.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,46 @@ document.querySelectorAll(".clickable-row[data-url]").forEach(row => {
2626
window.location.assign(row.dataset.url);
2727
});
2828
});
29+
30+
const requestSearch = document.getElementById("requestSearch");
31+
const methodFilter = document.getElementById("methodFilter");
32+
const statusFilter = document.getElementById("statusFilter");
33+
const resetFiltersBtn = document.getElementById("resetFiltersBtn");
34+
const visibleCount = document.getElementById("visibleCount");
35+
const emptyFilterState = document.getElementById("emptyFilterState");
36+
const requestRows = Array.from(document.querySelectorAll("#requestTable tbody tr.clickable-row"));
37+
38+
function applyRequestFilters() {
39+
if (!requestRows.length) return;
40+
41+
const search = (requestSearch?.value ?? "").trim().toLowerCase();
42+
const method = methodFilter?.value ?? "";
43+
const statusFamily = statusFilter?.value ?? "";
44+
let shown = 0;
45+
46+
requestRows.forEach(row => {
47+
const matchesSearch = !search || (row.dataset.search ?? "").toLowerCase().includes(search);
48+
const matchesMethod = !method || row.dataset.method === method;
49+
const matchesStatus = !statusFamily || row.dataset.statusFamily === statusFamily;
50+
const isVisible = matchesSearch && matchesMethod && matchesStatus;
51+
52+
row.hidden = !isVisible;
53+
if (isVisible) shown++;
54+
});
55+
56+
if (visibleCount) visibleCount.innerText = shown.toString();
57+
if (emptyFilterState) emptyFilterState.hidden = shown > 0;
58+
}
59+
60+
[requestSearch, methodFilter, statusFilter].forEach(control => {
61+
control?.addEventListener("input", applyRequestFilters);
62+
control?.addEventListener("change", applyRequestFilters);
63+
});
64+
65+
resetFiltersBtn?.addEventListener("click", () => {
66+
if (requestSearch) requestSearch.value = "";
67+
if (methodFilter) methodFilter.value = "";
68+
if (statusFilter) statusFilter.value = "";
69+
applyRequestFilters();
70+
requestSearch?.focus();
71+
});

0 commit comments

Comments
 (0)