Search docs

Jump to any table example

Discord—

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)LevelServiceRequestMessage
Sep 23 13:59:58infobilling200GET/billing/invoicesUser signed in
Sep 23 12:27:43warnauth429POST/auth/refreshRate limit reached for token
Sep 23 12:18:06infobilling304GET/billing/invoicesCache hit
Sep 23 11:17:31errorauth504POST/auth/refreshConnection pool exhausted (max=20)
Sep 23 09:47:17infoedge200GET/pricingRequest completed
Sep 23 09:30:24warnbilling401GET/billing/invoicesValidation failed: name is required
Sep 23 08:24:41infoedge200GET/Cache hit
Sep 23 06:42:48infoworker201POST/jobs/thumbnailRequest completed
Sep 23 05:26:44errorbilling502GET/billing/invoicesUpstream timed out after 10000ms
Sep 23 04:22:05debugbilling201POST/billing/webhookFeature flag new-pricing evaluated to false
Sep 23 02:22:26infoapi304DELETE/v1/projects/:idRequest completed
Sep 22 23:47:15debugauth201POST/auth/refreshFeature flag new-pricing evaluated to false
Sep 22 22:49:14warnbilling422GET/billing/invoicesValidation failed: name is required
Sep 22 21:50:56erroredge504GET/assets/app.jsStripe webhook signature mismatch
Sep 22 20:19:01infoworker201POST/jobs/thumbnailJob finished
Sep 22 18:18:29warnedge401GET/assets/app.jsValidation failed: name is required
Sep 22 16:44:15warnapi422POST/v1/projectsSlow query: 1843ms on projects_by_owner
Sep 22 15:06:20infoapi200GET/v1/projectsJob finished
Sep 22 13:06:07infoedge200GET/pricingInvoice generated
Sep 22 11:49:57warnedge401GET/assets/app.jsInvalid 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.

src/components/logs/side-panel-data-table.tsx
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.

src/components/logs/side-panel-data-table.tsx
const rows = table.getRowModel().rows
const 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.

src/components/logs/side-panel-data-table.tsx
function selectAt(index: number) {
const row = rows.at(index)
if (index < 0 || !row) return
setSelectedId(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.

src/components/logs/use-logs-table.ts
export function useLogsTable(options: LogsTableOptions) {
return useReactTable({
getRowId: (row) => row.id,
globalFilterFn: searchLogs,
getFilteredRowModel: getFilteredRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
// ...
...options,
})
}