Approaching the Data Explorer project, I identified the opportunity to make the filter sections configurable by target entity type – this way would could dynamically show filters that are relevant to the entity you’re looking at. A previous approach on this project by a peer had tightly coupled the UI components to the specific view the organization wanted to show – I wanted to make this reusable and to move on to new features. If we could create a configurable way to show relevant data we got a stable UI experience and the ability to spin up UI exponentially more quickly.
Filters vary by entity
The filters I’d show for “Users” aren’t the same as what I’d show for “Events” or “Organizations”. Some filter sections would need to use checkboxes, some text, some need a date range, etc.
I structured it so each entity owns a list of FilterConfig
entries. This keeps the UI declarative and avoids needing special-cased rendering logic everywhere.
type FilterConfig = {
id: string
label: string
type: 'checkbox' | 'text' | 'date-range'
field: string
operator?: string
sql?: string
displayValue?: (row: any) => string
dependsOn?: string[]
}
Example
The user table supports filtering by status, signup date, and email domain.
Here’s what that looks like in config:
const USER_FILTERS: FilterConfig[] = [
{
id: 'status',
label: 'Status',
type: 'checkbox',
field: 'status',
operator: 'IN',
sql: `
SELECT status AS value, COUNT(*) AS count
FROM users
GROUP BY status
`
},
{
id: 'signup_date',
label: 'Signup Date',
type: 'date-range',
field: 'created_at'
},
{
id: 'email_domain',
label: 'Email Domain',
type: 'checkbox',
field: 'email',
operator: 'LIKE',
sql: `
SELECT SPLIT_PART(email, '@', 2) AS value, COUNT(*) AS count
FROM users
GROUP BY value
ORDER BY count DESC
`,
displayValue: row => row.value.toUpperCase()
}
]
The config file gives us:
- Each entity full control over how it wants its filters to behave
- The query builder can consume the config and build correct SQL
- The UI can render the filter panel without needing knowledge of the entity
- It’s testable, composable, and easy to extend and maintain
What this unlocks
Because the filter panel is config-driven, we can:
- Fetch filter options dynamically based on the
sql
- Override display formatting with
displayValue
- Share filters across views by spreading configs
- Prepare for the ability for users to save custom data explorer views since we have a defined data type powering this view.
- Suggest AI-generated filters based on these keys – We can leverage our AI system to generate these configs for each user, intelligently adding information about the shape of that customer’s graph and outputting relevant filter views. Something I’m trying to make happen in our product.
It also sets us up to allow users to create their own saved views from these filters—since the filter inputs and output query are always in sync.
This kind of config layer makes dynamic UIs easier to scale. It’s not just about being DRY—it’s about making intent declarative and pushing behavior closer to the data layer.