From 9356060fed627113c61740585965e7760c8f5cc1 Mon Sep 17 00:00:00 2001 From: Steve W Date: Sun, 12 Apr 2026 01:14:07 +0000 Subject: [PATCH] feat: add queue filters and detail view --- src/App.css | 129 ++++++++++++++++++++++++++++- src/App.jsx | 234 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 327 insertions(+), 36 deletions(-) diff --git a/src/App.css b/src/App.css index abff3ee..69ff659 100644 --- a/src/App.css +++ b/src/App.css @@ -21,11 +21,19 @@ font-size: 0.8rem; } -h1 { +h1, +h2 { margin: 0; +} + +h1 { font-size: 2.4rem; } +h2 { + font-size: 1.1rem; +} + .subtitle { margin: 0.5rem 0 0; color: #94a3b8; @@ -33,7 +41,9 @@ h1 { .meta-card, .banner, -.table-card { +.table-card, +.detail-card, +.filters-card { background: #111827; border: 1px solid #1f2937; border-radius: 16px; @@ -59,6 +69,45 @@ h1 { color: #fecaca; } +.filters-card { + padding: 1rem; + margin-bottom: 1rem; +} + +.filter-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1rem; +} + +label { + display: flex; + flex-direction: column; + gap: 0.45rem; + color: #cbd5e1; +} + +label span { + font-size: 0.85rem; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +select { + background: #0b1220; + color: #e2e8f0; + border: 1px solid #334155; + border-radius: 10px; + padding: 0.7rem 0.85rem; +} + +.content-grid { + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr); + gap: 1rem; +} + .table-card { overflow: hidden; } @@ -83,8 +132,13 @@ th { letter-spacing: 0.04em; } -tr:hover td { - background: rgba(148, 163, 184, 0.06); +tbody tr { + cursor: pointer; +} + +tr:hover td, +.selected-row td { + background: rgba(148, 163, 184, 0.08); } .sort-button { @@ -109,6 +163,11 @@ tr:hover td { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } +.preserve { + white-space: pre-wrap; + word-break: break-word; +} + .status-badge { display: inline-flex; align-items: center; @@ -127,6 +186,53 @@ tr:hover td { .status-completed { background: #064e3b; color: #a7f3d0; } .status-cancelled { background: #374151; color: #d1d5db; } +.detail-card { + padding: 1rem; +} + +.detail-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.detail-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin: 0; +} + +.detail-row { + display: grid; + grid-template-columns: 120px 1fr; + gap: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid #1f2937; +} + +.detail-row dt { + color: #94a3b8; +} + +.detail-row dd { + margin: 0; +} + +.empty-state, +.empty-detail { + color: #94a3b8; + padding: 1rem; +} + +@media (max-width: 1100px) { + .content-grid { + grid-template-columns: 1fr; + } +} + @media (max-width: 900px) { .app-shell { padding: 1rem; @@ -137,6 +243,10 @@ tr:hover td { align-items: stretch; } + .filter-grid { + grid-template-columns: 1fr 1fr; + } + .table-card { overflow-x: auto; } @@ -144,4 +254,15 @@ tr:hover td { table { min-width: 760px; } + + .detail-row { + grid-template-columns: 1fr; + gap: 0.35rem; + } +} + +@media (max-width: 560px) { + .filter-grid { + grid-template-columns: 1fr; + } } diff --git a/src/App.jsx b/src/App.jsx index 1ecf29d..66278a9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -23,11 +23,19 @@ const columns = [ { key: 'created_at', label: 'Created At' }, ] +const defaultFilters = { + status: '', + type: '', + priority: '', + agent: '', +} + function shortId(id) { return id.slice(0, 8) } function formatDate(value) { + if (!value) return '—' return new Date(value).toLocaleString() } @@ -40,11 +48,28 @@ function compareValues(a, b, key) { return String(left).localeCompare(String(right)) } +function buildOptions(items, key) { + return [...new Set(items.map((item) => item[key]).filter(Boolean))].sort((a, b) => + String(a).localeCompare(String(b)), + ) +} + +function DetailRow({ label, value, mono = false, preserve = false }) { + return ( +
+
{label}
+
{value || '—'}
+
+ ) +} + function App() { const [items, setItems] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [sort, setSort] = useState({ key: 'created_at', direction: 'desc' }) + const [filters, setFilters] = useState(defaultFilters) + const [selectedId, setSelectedId] = useState('') useEffect(() => { let cancelled = false @@ -57,6 +82,10 @@ function App() { if (!cancelled) { setItems(data) setError('') + setSelectedId((current) => { + if (current && data.some((item) => item.id === current)) return current + return data[0]?.id ?? '' + }) } } catch (err) { if (!cancelled) setError(err.message || 'Failed to load queue') @@ -73,10 +102,37 @@ function App() { } }, []) + const filterOptions = useMemo( + () => ({ + statuses: buildOptions(items, 'status'), + types: buildOptions(items, 'type'), + priorities: buildOptions(items, 'priority'), + agents: buildOptions(items, 'assigned_agent'), + }), + [items], + ) + + const filteredItems = useMemo( + () => + items.filter((item) => { + if (filters.status && item.status !== filters.status) return false + if (filters.type && item.type !== filters.type) return false + if (filters.priority && String(item.priority) !== String(filters.priority)) return false + if (filters.agent && item.assigned_agent !== filters.agent) return false + return true + }), + [items, filters], + ) + const sortedItems = useMemo(() => { - const next = [...items].sort((a, b) => compareValues(a, b, sort.key)) + const next = [...filteredItems].sort((a, b) => compareValues(a, b, sort.key)) return sort.direction === 'asc' ? next : next.reverse() - }, [items, sort]) + }, [filteredItems, sort]) + + const selectedItem = useMemo( + () => sortedItems.find((item) => item.id === selectedId) || sortedItems[0] || null, + [sortedItems, selectedId], + ) function toggleSort(key) { setSort((current) => ({ @@ -85,6 +141,10 @@ function App() { })) } + function updateFilter(key, value) { + setFilters((current) => ({ ...current, [key]: value })) + } + return (
@@ -94,7 +154,8 @@ function App() {

Live queue view for all dispatched agent work items.

- {items.length} items + {filteredItems.length} visible items + {items.length} total items Polling every 10s
@@ -102,37 +163,146 @@ function App() { {error &&
{error}
} {loading &&
Loading queue…
} -
- - - - {columns.map((column) => ( - +
+
+
- - - {sortedItems.map((item) => ( - - - - - - - + + + + + + + + +
+
+
- -
{shortId(item.id)}{item.type} - {item.status} - {item.priority}{item.assigned_agent || '—'}{formatDate(item.created_at)}
+ + + {columns.map((column) => ( + + ))} - ))} - -
+ +
+ + + {sortedItems.map((item) => ( + setSelectedId(item.id)} + className={selectedItem?.id === item.id ? 'selected-row' : ''} + > + {shortId(item.id)} + {item.type} + + {item.status} + + {item.priority} + {item.assigned_agent || '—'} + {formatDate(item.created_at)} + + ))} + {!sortedItems.length && ( + + + No work items match the current filters. + + + )} + + +
+ +
)