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.

JupiterOne Data Explorer

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.