Config-driven filter panel
Each entity type declares its filters as data. One panel renders any of them, and saved views come almost for free.
Users, events, and devices each need different filters. Building a filter panel per entity means another bespoke screen every time one is added. Instead, each entity declares its filters as data, and one generic panel renders whatever it’s given.
type FilterConfig = {
id: string;
label: string;
type: "checkbox" | "text" | "date-range";
field: string;
op?: string;
options?: () => Promise<{ value: string; count: number }[]>; // live values with counts
display?: (value: string) => string;
};
const USER_FILTERS: FilterConfig[] = [
{ id: "status", label: "Status", type: "checkbox", field: "status", op: "IN",
options: () => api.valueCounts("users", "status") },
{ id: "created", label: "Signup date", type: "date-range", field: "created_at" },
];
<FilterPanel config={USER_FILTERS} filters={useFilters()} />
The panel switches on type to pick the control and loads options for its counts. It doesn’t know which entity it’s showing.
What the config gets you
- New entity, no new UI. Adding a view is writing a config.
- Shared filters. Spread a common config into several entities.
- Saved views. The config and the filter list are plain data, so a user’s view can be stored and restored.
- Generated configs. Since it’s just data, something else (like an AI that knows the shape of the user’s data) can write it.
The state behind it is deduped filter state.