-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary
When using AdHoc filters at the dashboard level, selecting a value for one filter scopes the available options for subsequent filters. For example, filtering last_name = "M." shows only 6 first names in the next filter's dropdown — the names that actually exist with that last name.
This is great UX because it:
- Helps users avoid "no data" queries
- Makes large datasets navigable through progressive refinement
- Matches user expectations from other filter UIs
However, the query builder's filter UI in panel edit mode does not scope filter values by preceding filters — it always shows all possible values for each dimension.
Current Behavior
AdHoc Filters (Dashboard): ✅ Scoped
- First filter:
last_name = M.(selected) - Second filter dropdown shows: 6 first names (only those with last name "M.")
Query Builder (Panel Edit): ❌ Not scoped
- First filter:
last_name = M.(selected) - Second filter dropdown shows: All first names in the dataset (regardless of last name)
⚠️ New addition here ⚠️
Additionally there's this case too:
I just found this, so describe it here, but not in the rest of this issue writeup.
Both together ❌ Not scoped
- AdHoc filters, filter:
last_name = M.(selected) - Query Builder (Panel Edit), filter: All first names in the dataset (regardless of last name)
See these screenshots:
- Filtered for Last names F, P, H in the AdHoc filter:

- But all last names available in the Panel-Edit filtering UI:

**
- Should this be one task (one PR) or two separate ones?
- Which should be done first?
Technical Analysis
Why AdHoc Filters Work
Grafana automatically passes existing filters to getTagValues() when fetching options for a new filter. Our datasource already handles this:
// src/datasource.ts
getTagValues(options: {
key: string;
filters?: Array<{ key: string; operator: string; value: string; values?: string[] }>;
}) {
const scopingFilters = options.filters?.length
? options.filters.map((filter) => ({ /* convert to Cube format */ }))
: undefined;
return this.getResource('tag-values', {
key: options.key,
filters: scopingFilters ? JSON.stringify(scopingFilters) : undefined,
});
}The Go backend already parses and applies these scoping filters:
// pkg/plugin/datasource.go
filtersJSON := parsedURL.Query().Get("filters")
if filtersJSON != "" {
var scopingFilters []map[string]interface{}
if err := json.Unmarshal([]byte(filtersJSON), &scopingFilters); err != nil {
backend.Logger.Warn("Failed to parse scoping filters, ignoring", "error", err)
} else if len(scopingFilters) > 0 {
cubeQuery["filters"] = scopingFilters
}
}Why Query Builder Doesn't Work
The useMemberValuesQuery hook doesn't pass any filters:
// src/queries.ts
export const useMemberValuesQuery = ({
datasource,
member,
}: {
datasource: DataSource;
member: string | null;
}) => {
return useQuery({
queryFn: async () => {
return await datasource.getTagValues({ key: member }); // No filters passed!
},
});
};Proposed Solution
The backend already supports filter scoping — this is purely a frontend change.
Files to Modify
src/queries.ts- Add optionalfiltersparameter touseMemberValuesQuerysrc/components/FilterField/FilterRow.tsx- Accept and pass preceding filterssrc/components/FilterField/FilterField.tsx- Compute and pass preceding filters to each row
Implementation Sketch
// queries.ts
export const useMemberValuesQuery = ({
datasource,
member,
precedingFilters, // NEW
}: {
datasource: DataSource;
member: string | null;
precedingFilters?: CubeFilter[];
}) => {
return useQuery({
queryKey: ['memberValues', datasource.uid, member, precedingFilters],
queryFn: async () => {
const formattedFilters = precedingFilters?.map(f => ({
key: f.member,
operator: f.operator === 'equals' ? '=' : '!=',
value: f.values[0] || '',
values: f.values,
}));
return await datasource.getTagValues({ key: member, filters: formattedFilters });
},
});
};// FilterField.tsx - pass preceding filters to each row
{filterStates.map((filter, index) => {
// Get all complete filters before this one
const precedingFilters = filterStates
.slice(0, index)
.filter((f): f is FilterState & { member: string } =>
f.member !== null && f.values.length > 0
)
.map(f => ({ member: f.member, operator: f.operator, values: f.values }));
return (
<FilterRow
key={index}
filter={filter}
precedingFilters={precedingFilters} // NEW
// ... other props
/>
);
})}Additional Context
Interestingly, the Cube UI itself (shown in screenshots) doesn't scope filter values by preceding filters either. This Grafana feature would actually provide better UX than Cube's native playground.
Acceptance Criteria
- When a filter row has a preceding filter with values selected, the dropdown for the current filter only shows values that exist given the preceding filter constraints
- The scoping updates reactively when preceding filter values change
- Empty/incomplete preceding filters (no member or no values) are excluded from scoping
- Query cache keys include preceding filters to prevent stale data