Shadcn Logs Table
A log explorer like the ones in observability tools: a date range picker, multi-select level, service, and status filters with live counts, and search that highlights its matches. Pick a layout for reading an event.
Click a row to show the event in a panel on the right. Move through events with the arrow keys or the panel buttons, and press Escape to close it.
$ npx shadcn add https://www.shad-table.dev/r/logs-table-side-panel.json| Time (UTC) | Level | Service | Request | Message |
|---|---|---|---|---|
| Sep 23 13:59:58 | info | billing | 200GET/billing/invoices | User signed in |
| Sep 23 12:27:43 | warn | auth | 429POST/auth/refresh | Rate limit reached for token |
| Sep 23 12:18:06 | info | billing | 304GET/billing/invoices | Cache hit |
| Sep 23 11:17:31 | error | auth | 504POST/auth/refresh | Connection pool exhausted (max=20) |
| Sep 23 09:47:17 | info | edge | 200GET/pricing | Request completed |
| Sep 23 09:30:24 | warn | billing | 401GET/billing/invoices | Validation failed: name is required |
| Sep 23 08:24:41 | info | edge | 200GET/ | Cache hit |
| Sep 23 06:42:48 | info | worker | 201POST/jobs/thumbnail | Request completed |
| Sep 23 05:26:44 | error | billing | 502GET/billing/invoices | Upstream timed out after 10000ms |
| Sep 23 04:22:05 | debug | billing | 201POST/billing/webhook | Feature flag new-pricing evaluated to false |
| Sep 23 02:22:26 | info | api | 304DELETE/v1/projects/:id | Request completed |
| Sep 22 23:47:15 | debug | auth | 201POST/auth/refresh | Feature flag new-pricing evaluated to false |
| Sep 22 22:49:14 | warn | billing | 422GET/billing/invoices | Validation failed: name is required |
| Sep 22 21:50:56 | error | edge | 504GET/assets/app.js | Stripe webhook signature mismatch |
| Sep 22 20:19:01 | info | worker | 201POST/jobs/thumbnail | Job finished |
| Sep 22 18:18:29 | warn | edge | 401GET/assets/app.js | Validation failed: name is required |
| Sep 22 16:44:15 | warn | api | 422POST/v1/projects | Slow query: 1843ms on projects_by_owner |
| Sep 22 15:06:20 | info | api | 200GET/v1/projects | Job finished |
| Sep 22 13:06:07 | info | edge | 200GET/pricing | Invoice generated |
| Sep 22 11:49:57 | warn | edge | 401GET/assets/app.js | Invalid refresh token |
Showing 20 of 80
How it works
1.The panel makes room by hiding columns
While an event is selected, columnVisibility hides Service and Request, whose values the panel header already shows, so the message column keeps enough width to read next to the panel.
const hiddenWhileOpen = { service: false, statusClass: false }const table = useLogsTable({data,columns,state: { columnVisibility: selectedId ? hiddenWhileOpen : {} },})
2.Selection is an id, checked against the visible rows
The panel stores the selected row id, not the row. Each render looks that id up in the current row model, so when a filter or search removes the event the panel closes instead of describing a row that is no longer in the table.
const rows = table.getRowModel().rowsconst selectedIndex = rows.findIndex((row) => row.id === selectedId)const selected = selectedIndex === -1 ? undefined : rows[selectedIndex]useEffect(() => {if (selectedId && !selected) setSelectedId(null)}, [selectedId, selected])
3.Arrow keys move through events
Rows are focusable. Up and down move both focus and the selection, so the panel follows the keyboard the way it does in a mail client, and the panel's own previous and next buttons use the same selectAt helper. Escape closes the panel from a row or from inside it.
function selectAt(index: number) {const row = rows.at(index)if (index < 0 || !row) returnsetSelectedId(row.id)document.getElementById(`log-row-${row.id}`)?.focus()}
4.Filters are shared with the expandable layout
Search, the date range, and the level, service, and status filters come from the same useLogsTable hook and LogsToolbar as the Expandable rows example. See that tab for how faceted counts, the date range, and search highlighting work.
export function useLogsTable(options: LogsTableOptions) {return useReactTable({getRowId: (row) => row.id,globalFilterFn: searchLogs,getFilteredRowModel: getFilteredRowModel(),getFacetedUniqueValues: getFacetedUniqueValues(),// ......options,})}