fix: DH-22976 ui.table filters to update when changed programmatically - #1377
fix: DH-22976 ui.table filters to update when changed programmatically#1377SimonVutov wants to merge 14 commits into
Conversation
|
ui docs preview (Available for 14 days) |
2e9c1ae to
8961441
Compare
|
ui docs preview (Available for 14 days) |
8961441 to
8eba401
Compare
|
ui docs preview (Available for 14 days) |
mofojed
left a comment
There was a problem hiding this comment.
This does allow the user to change the sorts, which is great. However, those changed sorts do not persist as they should (as described in my comment on #1358 (comment)).
I think to fix this properly (for both this case and the filters case described in DH-22976 that I've also assigned to you), we'll need changes in IrisGrid to handle this correctly. I think. I have a snippet specifically for sorting on #1358 (review)
|
ui docs preview (Available for 14 days) |
|
ui docs preview (Available for 14 days) |
1 similar comment
|
ui docs preview (Available for 14 days) |
There was a problem hiding this comment.
Add e2e test for this guy to make sure of the following:
- Filters/sorts update correctly when changed programmatically
- User can still change the filters and sorts, and those changes persist after refreshing
See an example e2e test with persistence https://github.com/deephaven/deephaven-plugins/pull/1379/changes#diff-297dc965f80789b8ed682341089a82d6f946c164dcab51bb3b08b34dff151a02
|
ui docs preview (Available for 14 days) |
|
Had to re-run the e2e tests a few times. Summary:
|
There was a problem hiding this comment.
This doesn't seem to work correctly for filtering, using the snippet from the ticket:
from deephaven import ui
import deephaven.plot.express as dx
_stocks = dx.data.stocks()
t = _stocks.where("Sym = `CAT`") # Applied when query is run
@ui.component
def table_filter_picker():
exchange, set_exchange = ui.use_state("NYPE")
return ui.flex(
ui.picker("NYPE", "PETX", on_selection_change=set_exchange, selected_key=exchange, label="Exchange"),
ui.heading(exchange),
ui.table( # Filters applied when table is opened on the client
_stocks,
show_quick_filters=True,
quick_filters={
"Sym": "CAT",
"Exchange": exchange,
"Price": ">=100"
}
),
direction="column"
)
t2 = table_filter_picker()Changing the dropdown does not update the filters.
EDIT: I see I probably need deephaven/web-client-ui#2712 as well. Put that in the PR description so it's clear; we shouldn't merge this until that fix goes in.
| // re-applied whenever their value changes. We stabilize them by content so an | ||
| // unrelated re-render (new reference, identical content) does not re-apply and | ||
| // replace changes the user made in the UI. | ||
| const stableSorts = useIsEqualMemo(sorts, deepEqual); |
There was a problem hiding this comment.
This shouldn't be necessary - we use patching in the deephaven.ui render so it should be a stable object unless there's actually changes. If that's not the case, there's something else that needs to be fixed in the renderer.
We added patching in: #1313
|
ui docs preview (Available for 14 days) |
mofojed
left a comment
There was a problem hiding this comment.
After playing with this a bit... I think we're going to want some design input from @dsmmcken on this. For example, when using the controlled quick_filters prop, e.g.
from deephaven import ui
import deephaven.plot.express as dx
_stocks = dx.data.stocks()
t = _stocks.where("Sym = `CAT`") # Applied when query is run
@ui.component
def table_filter_picker():
exchange, set_exchange = ui.use_state("NYPE")
return ui.flex(
ui.picker("NYPE", "PETX", on_selection_change=set_exchange, selected_key=exchange, label="Exchange"),
ui.heading(exchange),
ui.table( # Filters applied when table is opened on the client
_stocks,
show_quick_filters=True,
quick_filters={
"Sym": "CAT",
"Exchange": exchange,
"Price": ">=100"
}
),
direction="column"
)
t2 = table_filter_picker()
If I try and change one of the filters that are "controlled", it just reverts immediately (e.g. trying to change CAT to DOG:
Screencast.from.2026-07-06.10-55-16.mp4
I can still set other filters though (such as the Side I can filter to `buy).
Also this would be a breaking change of current behaviour, so we can't merge this as is. Could instead flip the params (e.g. quick_filter keeps existing behaviour, whereas controlled_quick_filter is one that updates programmatically... kind of janky).
So a couple design questions and/or things to consider before merging:
- Naming of prop - do we keep
quick_filterwith the existing uncontrolled behaviour, and then have controlled behaviour as another prop? - For true controlled behaviour, we'd need to have an
on_quick_filter_changedevent so you can process the new filter/apply it in the controlled mode. (This would probably be good to have regardless)
|
ui docs preview (Available for 14 days) |
…ters prop actually differs
|
ui docs preview (Available for 14 days) |
|
ui docs preview (Available for 14 days) |
|
ui docs preview (Available for 14 days) |
| // Stabilize the raw controlled values so an unrelated re-render can't hand | ||
| // us a new-but-equal-content object/array (see `useStableValue` above). | ||
| const stableControlledSorts = useStableValue(controlledSorts); | ||
| const stableControlledQuickFilters = useStableValue(controlledQuickFilters); | ||
|
|
||
| // The controlled values are live IrisGrid props. They are deliberately named | ||
| // separately so changing the existing user-owned props remains non-breaking. | ||
| const hydratedControlledSorts = useMemo(() => { | ||
| if ( | ||
| stableControlledSorts === undefined || | ||
| utils == null || | ||
| columns.length === 0 | ||
| ) { | ||
| return undefined; | ||
| } | ||
| log.debug('Hydrating controlled sorts', stableControlledSorts); | ||
| return utils.hydrateSort(columns, stableControlledSorts); | ||
| }, [stableControlledSorts, utils, columns]); | ||
|
|
||
| const hydratedControlledQuickFilters = useMemo( | ||
| () => | ||
| hydrateUITableQuickFilters( | ||
| stableControlledQuickFilters, | ||
| model, | ||
| columns, | ||
| utils | ||
| ), | ||
| [stableControlledQuickFilters, model, columns, utils] | ||
| ); | ||
|
|
There was a problem hiding this comment.
We shouldn't need to do this. With documentPatch we should only be sent updates about JSON that has changed, the object should already be the same.
I made a similar comment previously: #1377 (comment)
There was a problem hiding this comment.
You're right, we don't need it. Thanks for catching that.
|
ui docs preview (Available for 14 days) |
There was a problem hiding this comment.
Pull request overview
Adds controlled table sorts and quick filters while preserving user-owned grid state.
Changes:
- Introduces controlled Python and TypeScript table props.
- Separates initial state hydration from live controlled updates.
- Adds Python and E2E coverage for sorting and filtering.
Reviewed changes
Copilot reviewed 7 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/utils.ts |
Adds grid interaction helpers. |
tests/ui_table.spec.ts |
Adds sorting/filtering E2E scenarios. |
tests/app.d/ui_table.py |
Adds E2E table fixtures. |
plugins/ui/test/deephaven/ui/test_ui_table.py |
Tests controlled prop serialization and validation. |
plugins/ui/src/js/src/elements/UITable/UITableUtils.ts |
Defines controlled frontend props. |
plugins/ui/src/js/src/elements/UITable/UITable.tsx |
Hydrates initial and controlled grid state. |
plugins/ui/src/deephaven/ui/components/table.py |
Exposes and validates controlled Python APIs. |
package-lock.json |
Updates dependency metadata. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const hydratedControlledSorts = useMemo(() => { | ||
| if ( | ||
| controlledSorts === undefined || | ||
| utils == null || | ||
| columns.length === 0 | ||
| ) { | ||
| return undefined; | ||
| } | ||
| log.debug('Hydrating controlled sorts', controlledSorts); | ||
| return utils.hydrateSort(columns, controlledSorts); | ||
| }, [controlledSorts, utils, columns]); |
| // DH-22976: Existing user-owned sorts and quick filters persist after refresh. | ||
| test('UI table user sorts and filters persist after refresh', async ({ | ||
| page, | ||
| }) => { |
| const hydratedControlledQuickFilters = useMemo( | ||
| () => | ||
| hydrateUITableQuickFilters(controlledQuickFilters, model, columns, utils), | ||
| [controlledQuickFilters, model, columns, utils] | ||
| ); |
My opinion is we should do the breaking change so that we can be internally consistent between all our components, where quick filters is controlled, and default_quick_filters is uncontrolled just like all our other components. Is it likely we would actually be breaking workflows given that the existing behaviour was pretty limited in usefulness? |
mofojed
left a comment
There was a problem hiding this comment.
There's still some UX here we'll need to discuss with @dsmmcken ... E.g.
- If there is a controlled filter applied, currently you can still edit it, but then it just doesn't take effect because the field is controlled. I think we need two additional things:
- An
on_quick_filters_changedhandler that you can wire up and get the input as the user changes the state, and react to it (we should have this anyways). That way you can actually have controlled input where the user can still change it. - A
is_quick_filters_read_onlyto put the quick filters in a state where user cannot edit them (should it just beis_filters_read_onlyor a generalis_read_onlyinstead?).
- An
Also there seems to be an issue currently where I can change the quick filters when using controlled_quick_filters, and it seems to be taking it every other time (see the second row, as it's filtered out and then comes back as the filter applies then unapplies):
Screencast.from.2026-08-13.10-36-49.mp4
It shouldn't be applying at all, since we have controlled filters.
| quickFilters, | ||
| controlledQuickFilters, |
There was a problem hiding this comment.
After discussing further with @dsmmcken , we're just going to make this a breaking change and put this in the v1.x of deephaven.ui - we want the API to be consistent to all the spectrum components.
E.g. Renaming
quickFilters/quick_filters => defaultQuickFilters/default_quick_filters
controlledQuickFilters/controlled_quick_filters => quickFilters/quick_filters
Then it's consistent with the other APIs.
| quick_filters: The initial quick filters to apply to the table. User changes | ||
| are retained and persisted when the table is reloaded. Dictionary of | ||
| column name to filter value. | ||
| controlled_quick_filters: The quick filters to update programmatically. |
There was a problem hiding this comment.
In addition to controlled filters, we need on_quick_filters_change and on_sorts_change as well.
mofojed
left a comment
There was a problem hiding this comment.
There's still some UX here we'll need to discuss with @dsmmcken ... E.g.
- If there is a controlled filter applied, currently you can still edit it, but then it just doesn't take effect because the field is controlled. I think we need two additional things:
- An
on_quick_filters_changedhandler that you can wire up and get the input as the user changes the state, and react to it (we should have this anyways). That way you can actually have controlled input where the user can still change it. - A
is_quick_filters_read_onlyto put the quick filters in a state where user cannot edit them (should it just beis_filters_read_onlyor a generalis_read_onlyinstead?).
- An
Also there seems to be an issue currently where I can change the quick filters when using controlled_quick_filters, and it seems to be taking it every other time (see the second row, as it's filtered out and then comes back as the filter applies then unapplies):
Screencast.from.2026-08-13.10-36-49.mp4
It shouldn't be applying at all, since we have controlled filters.
|
is_quick_filters_read_only seems like the sort of thing that would belong in #282 as its one of the many many things that could be disabled. |
…ilters. Breaking change implemented
|
ui docs preview (Available for 14 days) |
|
I saw changes to sorts as well as filter, so I altered the test script to test the sorts. This appears to work for sorts: from deephaven import ui
import deephaven.plot.express as dx
_stocks = dx.data.stocks()
t = _stocks.where("Sym = `CAT`") # Applied when query is run
@ui.component
def table_filter_picker():
sort, set_sort = ui.use_state("ASC")
is_abs, set_is_abs = ui.use_boolean()
return ui.flex(
ui.picker("ASC", "DESC", on_selection_change=set_sort, selected_key=sort, label="Index sort"),
ui.checkbox(is_selected=is_abs, on_change=set_is_abs),
ui.heading(sort),
ui.table( # Filters applied when table is opened on the client
_stocks,
show_quick_filters=True,
sorts={"column": "Random", "direction": sort, "is_abs": is_abs},
),
direction="column"
)
t2 = table_filter_picker() |
margaretkennedy
left a comment
There was a problem hiding this comment.
Docs changes look fine
Merge this web client UI PR first: deephaven/web-client-ui#2737
Demo
The following code is used for this demo (from the ticket):
When we use controlled
quick_filterson_quick_filters_changehandler is provided. To accept user edits, provideon_quick_filters_changeand update the parent-owned filter state from that callback.When we use
default_quick_filtersChanging
quick_filters={...todefault_quick_filters={