Skip to content

Shared Filters (MeterFilterBar)

Use one page-level filter state to drive MeterUsage, MeterChart, and MeterTable. The core package owns the state; the React/Vue packages render the controls and subscribe the page to changes.

Loading interactive preview…

View codeHide code

Actual React source for this preview, including demo controls, shared styles and isolated mocks. The docs host supplies the client and theme; application installation and React / Vue usage follow below.

import { useMemo } from "react"
import {
MeterFilterBar,
useMeterFilters,
} from "@meterry/component-react"
import {
createMeterFilterController,
type MeterThemeInput,
} from "@meterry/component"
import { MODELS, type PreviewEnvironment } from "./preview-data"
import { styleProps } from "./preview-shared"
import { UsagePreview } from "./usage-preview"
import { ChartPreview } from "./chart-preview"
import { TablePreview } from "./table-preview"
export function SharedFiltersPreview({
environment,
theme,
}: {
environment: PreviewEnvironment
theme: MeterThemeInput
}) {
const controller = useMemo(
() =>
createMeterFilterController([
{
key: "model",
label: "Model",
type: "select",
options: MODELS.map((value) => ({ label: value, value })),
},
{
key: "provider",
label: "Provider (exact)",
type: "text",
placeholder: "openai, anthropic, or r9s",
},
]),
[]
)
const filters = useMeterFilters(controller)
const activeFilters = [
filters.model && `Model: ${filters.model}`,
filters.provider && `Provider: ${filters.provider}`,
]
.filter(Boolean)
.join(" · ")
return (
<div className="docs-preview-stack">
<MeterFilterBar
{...styleProps}
controller={controller}
debounceMs={300}
messages={{ filterBarLabel: "Shared dashboard filters" }}
/>
<output
className="docs-preview-filter-summary"
data-testid="committed-filters"
>
{activeFilters
? `Active filters: ${activeFilters}`
: "All models and providers"}
</output>
<details className="docs-preview-options">
<summary>Connected Usage, Chart &amp; Table</summary>
<div className="docs-preview-options-body">
<UsagePreview
environment={environment}
theme={theme}
filters={filters}
/>
<ChartPreview
environment={environment}
theme={theme}
filters={filters}
/>
<TablePreview
environment={environment}
sharedController={controller}
/>
</div>
</details>
</div>
)
}
Terminal
npm install @meterry/component @meterry/component-react echarts echarts-for-react

For default styles, optionally install @meterry/component-tailwindcss. For authentication, shared clients and theming, see Getting Started.

Connect filter values directly to both wrappers instead of writing request/pivot/reload effects:

function UsageViews({ controller }: { controller: MeterFilterController }) {
const filters = useMeterFilters(controller)
return <>
<MeterUsageTable source="usage"
query={{ metrics: ['input_tokens'], group_by: ['bucket', 'model', 'metric'], filters }} />
<MeterUsageChart type="line"
query={{ metrics: ['input_tokens', 'output_tokens'], group_by: ['bucket'], filters }} />
</>
}

Import MeterUsageTable, MeterUsageChart and useMeterFilters from the framework package, and MeterFilterController from core. Render under a shared client provider. The wrappers own query changes and cancellation. source="events" only accepts time/metric selectors in the current wrapper, not arbitrary shared dimension filters. The rest of this page explains the still-supported lower-level MeterTable / MeterChart integration.

Change Model or enter an exact Provider to update Usage, Chart, and the remote Table together. Text commits after 300ms; selects commit immediately. Table-local Metric and Quantity filters remain independent when you clear the shared bar.

MeterFilterBar → MeterFilterController → page subscriptions
├── MeterUsage query.filters
├── filtered MeterChart data
└── filtered MeterTable data or a remote load

The page below receives an authenticated SubjectClient and already-fetched rows. Keep the controller and column definitions stable; changing filters should not recreate them.

import { useMemo } from 'react'
import {
MeterChart, MeterClientProvider, MeterFilterBar, MeterTable,
MeterUsage, useMeterFilters,
} from '@meterry/component-react'
import {
createMeterFilterController,
type MeterFilterDefinition,
type SubjectClient,
type TableColumn,
} from '@meterry/component'
type Row = { model: string; provider: string; quantity: number }
const definitions: MeterFilterDefinition[] = [
{
key: 'model', label: 'Model', type: 'select',
options: [
{ label: 'GLM', value: 'glm-5.1' },
{ label: 'GPT', value: 'gpt-4o' },
],
},
{ key: 'provider', label: 'Provider', type: 'text', placeholder: 'Exact provider name' },
]
const columns: TableColumn<Row>[] = [
{ key: 'model', header: 'Model' },
{ key: 'provider', header: 'Provider' },
{ key: 'quantity', header: 'Quantity', sortable: true },
]
export function Dashboard({ client, rows }: { client: SubjectClient; rows: Row[] }) {
const controller = useMemo(() => createMeterFilterController(definitions), [])
const filters = useMeterFilters(controller)
// This page deliberately uses exact dimension matching for local data.
const filteredRows = useMemo(() => rows.filter((row) =>
(!filters.model || row.model === filters.model) &&
(!filters.provider || row.provider === filters.provider),
), [rows, filters])
return (
<MeterClientProvider client={client}>
<MeterFilterBar controller={controller} debounceMs={300} />
<MeterUsage query={{ metrics: ['tokens'], filters }} />
<MeterChart
type="bar" data={filteredRows} x="model"
series={[{ key: 'quantity', value: 'quantity' }]}
/>
<MeterTable columns={columns} data={filteredRows} pageSize={10} />
</MeterClientProvider>
)
}

Local filtering only sees rows already loaded into the browser. For complete dashboard comparisons, fetch those rows for the same time window and measures as the Usage query. Use remote filtering instead when the browser only holds a page or subset of the data.

Call useMeterFilterValues inside setup / <script setup> (or an active effect scope). It returns a Ref<MeterFilterValues> and unsubscribes when that scope is disposed. Use .value in script; Vue unwraps the ref in templates.

<script setup lang="ts">
import { computed } from 'vue'
import {
MeterChart, MeterFilterBar, MeterTable, MeterUsage,
provideMeterClient, useMeterFilterValues,
} from '@meterry/component-vue'
import {
createMeterFilterController,
type SubjectClient,
type TableColumn,
} from '@meterry/component'
type Row = { model: string; provider: string; quantity: number }
const props = defineProps<{ client: SubjectClient; rows: Row[] }>()
provideMeterClient({ client: props.client })
const controller = createMeterFilterController([
{
key: 'model', label: 'Model', type: 'select',
options: [
{ label: 'GLM', value: 'glm-5.1' },
{ label: 'GPT', value: 'gpt-4o' },
],
},
{ key: 'provider', label: 'Provider', type: 'text', placeholder: 'Exact provider name' },
])
const filters = useMeterFilterValues(controller)
const filteredRows = computed(() => props.rows.filter((row) =>
(!filters.value.model || row.model === filters.value.model) &&
(!filters.value.provider || row.provider === filters.value.provider),
))
const columns: TableColumn<Row>[] = [
{ key: 'model', header: 'Model' },
{ key: 'provider', header: 'Provider' },
{ key: 'quantity', header: 'Quantity', sortable: true },
]
</script>
<template>
<MeterFilterBar :controller="controller" :debounce-ms="300" />
<MeterUsage :query="{ metrics: ['tokens'], filters }" />
<MeterChart
type="bar" :data="filteredRows" x="model"
:series="[{ key: 'quantity', value: 'quantity' }]"
/>
<MeterTable :columns="columns" :data="filteredRows" :page-size="10" />
</template>

When the page already owns its values, omit controller and pass definitions, values, and a change handler. The handler receives the next complete value record, not a single changed field.

React (using the definitions from above):

import { useState } from 'react'
import { MeterFilterBar, MeterUsage } from '@meterry/component-react'
import { normalizeMeterFilters, type MeterFilterValues } from '@meterry/component'
function ControlledFilters() {
const [values, setValues] = useState<MeterFilterValues>({})
return (
<>
<MeterFilterBar definitions={definitions} values={values} onChange={setValues} />
{/* Render under a MeterClientProvider, or pass an explicit client. */}
<MeterUsage query={{ filters: normalizeMeterFilters(values) }} />
</>
)
}

Vue (in page setup, using the same definitions):

<script setup lang="ts">
import { ref } from 'vue'
import { MeterFilterBar } from '@meterry/component-vue'
import type { MeterFilterValues } from '@meterry/component'
// Define or import the definitions shown above.
const values = ref<MeterFilterValues>({})
const onChange = (next: MeterFilterValues) => { values.value = next }
</script>
<template>
<MeterFilterBar :definitions="definitions" :values="values" @change="onChange" />
</template>

This is values + @change, not v-model: the component does not emit update:values. With neither controller nor values, the bar maintains internal values and still reports changes through onChange / @change.

Choose one state owner

When controller is present it takes precedence over values, and changes go to the controller rather than onChange / @change. Subscribe to the controller to observe them. definitions can override the rendered controls, but does not replace the controller’s definitions; Clear clears all controller values, including keys not rendered by that bar.

  • Text and number filters apply after you pause typing for 300ms. Select filters apply immediately.
  • Changing a select does not submit unfinished text or number input in another field. Each field waits for its own input to finish.
  • Set debounceMs={0} (React) or :debounce-ms="0" (Vue) to apply text/number changes immediately, or choose a longer delay for your page.
  • Clear removes all shared filters and cancels unfinished input. It does not clear the table’s separate column filters.
  • To change filters from another page control, call controller.setValue(key, value) or controller.setValues(values).

When filters change, the connected Usage, Chart, and Table views show the new selection. See MeterUsage and MeterTable for loading, empty, and error messages.

Filter values are strings, including values from number inputs. An empty string removes that condition; '0' is a valid value. Use a definition’s defaultValue or supply initial values when creating the controller:

import {
createMeterFilterController, mergeMeterFilters, normalizeMeterFilters,
} from '@meterry/component'
const controller = createMeterFilterController(
[{ key: 'model', type: 'text', defaultValue: 'gpt-4o' }],
{ model: '' },
)
controller.getValues() // {}: the explicit blank clears the default
normalizeMeterFilters({ model: '', quantity: '0' }) // { quantity: '0' }
mergeMeterFilters({ model: 'gpt-4o', provider: 'openai' }, { model: '' })
// { provider: 'openai' }: a later explicit blank removes an earlier key
  • Defaults apply when the controller is created. Clear removes the conditions rather than restoring defaults. For a bar without a controller, initialize the page’s values explicitly.
  • Use setValues to replace all shared filters, or setValue to change one condition.
  • Use normalizeMeterFilters to remove empty values before making your own query, and mergeMeterFilters to combine shared and table-local filters.
  • MeterUsage removes empty dimension filters automatically. Metric and date selectors should update query.metrics or the time range instead of being sent as dimensions.

definition.type chooses the input control; it is not a query operator. The shared controller does not implement contains, numeric comparison, date ranges, multi-select, or a match callback. Those require application-side matching and/or conversion to the service’s supported request format.

For instance, the dashboard examples above deliberately use equality for both local dimensions, even though Provider uses a text input. Check that this matches your service’s rules. Do not use substring matching locally while expecting exact matching remotely and assume both components will return the same result. A metric selector or date range may need to map to query.metrics or startTime / endTime, rather than blindly becoming a dimension filter.

Table-local column filters have their own rules: text defaults to case-insensitive contains, select to equality, and number to numeric equality; column.filter.match can override matching in local-data mode. See TableColumn.

For a local table, pass the filtered rows through data. The table keeps its sort order and column filters and returns to page 1. Public/shared filters and table-local filters remain independent; Clear on the shared bar does not clear the table’s column filters.

For remote tables, merge shared values inside load and explicitly trigger a reload on shared changes. Simply reading controller.getValues() in a stable load does not subscribe the table to the controller.

The following wiring assumes the page already owns controller, stable columns, and an application loadRows(query) function. loadRows must query the service, map rows, and implement the requested sorting/pagination before returning { rows, total }.

import { createTableController, mergeMeterFilters } from '@meterry/component'
// Create once in page setup (useMemo in React).
const tableController = createTableController({
columns,
load: (query) => loadRows({
...query,
filters: mergeMeterFilters(query.filters, controller.getValues()),
}),
})
// Register in useEffect (React), or setup with onScopeDispose (Vue).
const unsubscribe = controller.subscribe(() => {
void tableController.setPage(1) // resets pagination and loads once
})
// Invoke unsubscribe on cleanup. Render MeterTable with this tableController.

Here shared values win when keys overlap. Prefer distinct keys for shared and local concerns; merging cannot represent two conflicting predicates on one key. A missing shared key does not delete a table-local key, so clearing shared filters can reveal a still-active local filter.

Create the table controller once and reload it when shared filters change, rather than creating a new table for every selection. For an example, see MeterTable: shared filters.

Prop Type Default Description
definitions MeterFilterDefinition[] Controller definitions, or [] Controls to render
controller MeterFilterController — Shared state owner; takes precedence over values
values MeterFilterValues Internal {} Page-owned committed values when no controller is supplied
onChange / @change (values: MeterFilterValues) => void — Next full value set in controlled/standalone modes
debounceMs / :debounce-ms number 300 Text/number input delay in milliseconds; select changes apply immediately
messages Partial<MeterMessages> English defaults Labels and placeholders
className / class string — Class on the bar’s root node
classNames / :class-names MeterClassNames — Override the filterBar, filter, and button slots
unstyled boolean false Disable default semantic classes

Use messages.filterBarLabel to name the filter group and definition.label to label each control. Customize its appearance with the Tailwind plugin or classNames.filterBar, classNames.filter, and classNames.button.

Copy shared with table controls: clearFilters, allFilterOption, and filterPlaceholder(label). See Internationalization for an override example.

API details: core filter controller, React, and Vue.