Skip to content

Commit 9fdca3c

Browse files
authored
feat: blog post data-table-filters (#1983)
* feat: blog post data-table-filters * chore: update blog post
1 parent 068338e commit 9fdca3c

File tree

6 files changed

+164
-0
lines changed

6 files changed

+164
-0
lines changed
879 KB
Loading
637 KB
Loading
356 KB
Loading
647 KB
Loading
67.1 KB
Loading
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
title: "Nobody should hand-code a data table in 2026"
3+
description: "We have rebuilt the data-table-filters with a single schema, state management adapters, shadcn registry distribution, and an AI agent skill."
4+
author: "Maximilian Kaske"
5+
publishedAt: "2026-03-16"
6+
image: "/assets/posts/nobody-should-hand-code-a-data-table/nobody-should-hand-code-a-data-table.png"
7+
category: "engineering"
8+
---
9+
10+
The [data-table-filters](https://data-table.openstatus.dev?referrer=openstatus) project has been live for 1.5+ years. Filters, sorting, infinite scroll — the whole thing. It worked. People starred it. Bigger companies like [Supabase](https://supabase.com?referrer=openstatus) took inspiration for their logs dashboard.
11+
12+
But adopting it in another project wasn't straightforward. You'd have to clone the repo, wire together multiple files just to add a column — columns in one file, filter config in another, sheet fields in a third, Zod schema somewhere else. Change one, forget the others, wonder why nothing works.
13+
14+
> It was a good idea. It was a bad DX.
15+
16+
Then the **[shadcn](https://ui.shadcn.com?referrer=openstatus)** components registry and **agent skills** landed — and suddenly the pieces fit. We can ship code people own and an AI that knows how to set it up. No CLI installer needed. No npm package needed. And for AI coding tools, importing files via CLI keeps the context window lean — no need to overbloat it by copy-pasting entire repos worth of code.
17+
18+
We rebuilt the entire developer experience — refactoring code, improving composability, and shipping modern distribution — without compromising the UI taste. Here's what we shipped.
19+
20+
---
21+
22+
<Image src="/assets/posts/nobody-should-hand-code-a-data-table/data-table-filters-homepage.png" alt="The data-table-filters homepage." />
23+
24+
---
25+
26+
## State management adapters
27+
28+
Before, the table was married to [nuqs](https://nuqs.dev?referrer=openstatus) (URL search params). If you didn't want URL state, tough luck.
29+
30+
We built a BYOS (Bring Your Own Store) architecture with three adapters that all implement the same interface:
31+
32+
- **Memory** — state lives in React refs. No URL, no external store. The default when getting started.
33+
- **[nuqs](https://nuqs.dev?referrer=openstatus)** — state syncs to URL search params. Shareable links, back button works, SSR-friendly. The original behavior, now pluggable.
34+
- **[zustand](https://zustand.docs.pmnd.rs?referrer=openstatus)** — state lives in a zustand store via a `createFilterSlice()` helper. Drops into existing zustand setups.
35+
36+
All three support pause/resume (for live-streaming mode) and work with React 18's `useSyncExternalStore`. Swapping adapters is one line — the table doesn't know or care which one you're using.
37+
38+
Adding a new store to the mix is straightforward too. Just implement the `StoreAdapter<T>` interface and you're good to go.
39+
40+
## Single source of truth: the table schema
41+
42+
This was the big refactor. Before, defining a table meant keeping five separate files in sync:
43+
44+
- `columns.tsx` — TanStack column definitions, cell renderers, sizing
45+
- `constants.tsx` — filter field configs, sheet field configs, UI properties
46+
- `schema.ts` — Zod validation for data rows AND a separate BYOS filter schema with serialization delimiters
47+
- `search-params.ts` — nuqs parser derived from the filter schema
48+
- `store.ts` — zustand slice derived from the filter schema
49+
50+
Adding a column? Edit all five. Renaming a field? All five. Changing a filter type from checkbox to slider? Update the schema, the constants, and the columns file — and hope you didn't miss the filterFn.
51+
52+
The solution: define a column once, derive everything else.
53+
54+
```tsx
55+
export const tableSchema = createTableSchema({
56+
level: col
57+
.enum(LEVELS)
58+
.label("Level")
59+
.filterable("checkbox", {
60+
options: LEVELS.map((l) => ({ label: l, value: l })),
61+
})
62+
.defaultOpen()
63+
.size(27),
64+
65+
date: col
66+
.timestamp()
67+
.label("Date")
68+
.display("timestamp")
69+
.sortable()
70+
.defaultOpen(),
71+
72+
latency: col
73+
.number()
74+
.label("Latency")
75+
.display("number", { unit: "ms" })
76+
.filterable("slider"),
77+
});
78+
```
79+
80+
One definition. Four generators derive columns, filter fields, filter schema, and sheet fields automatically. Five files collapsed into one `table-schema.tsx`.
81+
82+
This became the foundation for everything that followed — the builder, the Drizzle integration, the registry all depend on it.
83+
84+
It was also only possible to build because of all the extra work done before. It's fine to copy-paste things and not extract too early — until you see the pattern and can actually optimize for it.
85+
86+
## The schema builder
87+
88+
If the schema can be generated from a definition, why not generate it from raw data?
89+
90+
A [builder](https://data-table.openstatus.dev/builder?referrer=openstatus) where you paste JSON (or upload a CSV) and instantly get a working, filterable table. No column definitions. No config. Just data in, table out.
91+
92+
<Image src="/assets/posts/nobody-should-hand-code-a-data-table/builder-data-table.png" alt="The schema builder: JSON on the left, live filterable table on the right." />
93+
94+
The components aren't fully customizable in the builder (serialization constraints), but it helps you get started and understand how schema changes affect the data table.
95+
96+
The inference engine detects types and applies domain heuristics — keys with "latency" get millisecond units, "id" columns get monospace display, trace IDs are auto-hidden. We ship presets for common patterns like timestamps, durations, and log levels. Contributions welcome — the more opinionated, well-built presets we have, the better the out-of-the-box experience gets for specific use cases.
97+
98+
The preview uses the same infinite-scroll architecture as the real thing — what you see in the builder is exactly what you'll ship.
99+
100+
## Drizzle ORM: a real database with real data
101+
102+
A client-side demo only takes you so far. People need to see this working with a real database, real data, growing over time.
103+
104+
We built a full [Drizzle ORM](https://orm.drizzle.team?referrer=openstatus) integration connected to a [Supabase](https://supabase.com?referrer=openstatus) PostgreSQL database.
105+
106+
A [Vercel](https://vercel.com?referrer=openstatus) cron job runs every 10 minutes, generating realistic HTTP request logs — randomized timing metrics, status codes, multiple regions with latency multipliers. The data accumulates over time, so the demo always has fresh, realistic data to filter through — and it supports live mode (just time it right, the cron runs every 10 minutes).
107+
108+
The [`/drizzle`](https://data-table.openstatus.dev/drizzle?referrer=openstatus) route shows it all working together: infinite scroll with cursor-based pagination, faceted search with live mode, nuqs URL state so filters are shareable, and time-bucketed charts via PostgreSQL's `date_bin()`.
109+
110+
<Image src="/assets/posts/nobody-should-hand-code-a-data-table/drizzle-data-table.png" alt="The /drizzle route with live data, faceted filters, and time-bucketed charts." />
111+
112+
This is the kind of example that actually helps people adopt a library. Not "here's a static demo" — here's a production-like setup with a real database, real data pipeline, and all the pieces wired together.
113+
114+
## Tests. Lots of tests.
115+
116+
> Writing good tests is cheap nowadays. Just do it. Thank me later.
117+
118+
39 test files covering the table schema, store adapters, builder, Drizzle ORM (including a dedicated SQL injection suite), and utilities. Everything from column builder validation to `'; DROP TABLE logs; --`.
119+
120+
CI runs against a real PostgreSQL container — migrations, seed data, then tests. No mocks for the database layer. We want this production-ready, so making sure test coverage is solid and tests are green is non-negotiable.
121+
122+
## Distribution killer: shadcn registry + agent skill
123+
124+
A GitHub issue ([#39](https://github.com/openstatushq/data-table-filters/issues/39?referrer=openstatus)) put it plainly: "Can you please provide it as a package, so that it could easily be installed and managed?" Another ([#14](https://github.com/openstatushq/data-table-filters/issues/14?referrer=openstatus)) asked for a Vite example, which would've meant restructuring into a monorepo.
125+
126+
The **[shadcn registry](https://ui.shadcn.com/docs/registry?referrer=openstatus)** solves the distribution problem. Adopting it required migrating to Tailwind v4 first (which was long overdue!) — the registry blocks declare CSS variables using `@theme inline` syntax, so v4 was a prerequisite. Then we created a `registry.json` with 9 installable blocks:
127+
128+
```bash
129+
npx shadcn@latest add https://data-table.openstatus.dev/r/data-table.json
130+
```
131+
132+
One command. Dependencies resolved. CSS variables injected. Path aliases rewritten. Install just the core, or add Drizzle helpers, command palette, zustand adapter — whatever you need.
133+
134+
Then there's the **agent skill**.
135+
136+
Think about what a CLI installer does: it asks you questions ("TypeScript?", "Which state manager?", "Do you use Drizzle?"), then generates files based on your answers. It's a decision tree pretending to be a conversation.
137+
138+
An agent skill is an actual conversation. Install it with:
139+
140+
```bash
141+
npx skills add https://github.com/openstatushq/data-table-filters --skill data-table-filters
142+
```
143+
144+
When someone opens their AI coding tool and says "add a filterable data table", the skill activates.
145+
146+
<Image src="/assets/posts/nobody-should-hand-code-a-data-table/agent-data-table.png" alt="asking 'add a data-table example' - no extra guidance (after setting up the skill)." />
147+
148+
It understands the project, installs the right blocks, generates a schema from the data model, wires up the state adapter, and configures the database integration — if you ask for it. Start minimal and expand to your use case.
149+
150+
We also rewrote all the [docs](https://data-table.openstatus.dev/docs?referrer=openstatus) from scratch. Not glamorous work — but when an AI reads your docs to install your library, doc quality directly affects agent quality. **Better docs, better agent.**
151+
152+
## Where this is going
153+
154+
We keep improving data-table-filters to make it the fastest way to spin up large, production-ready dataset views for logs and beyond.
155+
156+
> It's not a library. It's a playbook.
157+
158+
The combination of shadcn registries and agent skills is a really good distribution model for frontend libraries. You don't publish a package with a fixed API. You ship composable code blocks, matching your design system, that an AI understands how to assemble.
159+
160+
Not "install this package and read the docs." More like "tell your AI what you need and it builds it from well-structured, composable pieces."
161+
162+
> Nobody should hand-code a data table anymore.
163+
164+
The full project is open source at [data-table-filters](https://github.com/openstatushq/data-table-filters?referrer=openstatus). Try the builder at [data-table.openstatus.dev/builder](https://data-table.openstatus.dev/builder?referrer=openstatus). Or just open your favorite chat interface, install the agent skill, and say "add a filterable data table" — the skill will take it from there.

0 commit comments

Comments
 (0)