Skip to content

fix: DH-22976 ui.table filters to update when changed programmatically - #1377

Open
SimonVutov wants to merge 14 commits into
mainfrom
feat-DH-21841-fix-user-to-modify
Open

fix: DH-22976 ui.table filters to update when changed programmatically#1377
SimonVutov wants to merge 14 commits into
mainfrom
feat-DH-21841-fix-user-to-modify

Conversation

@SimonVutov

@SimonVutov SimonVutov commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Merge this web client UI PR first: deephaven/web-client-ui#2737

Demo

The following code is used for this demo (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()

When we use controlled quick_filters

  • user can change Exchange from NYPE to PETX, and the filter reflects this in the table
  • user can NOT modify any of the filters by clicking on quick-filter cells. (for example Price or Dollars)
  • No on_quick_filters_change handler is provided. To accept user edits, provide on_quick_filters_change and update the parent-owned filter state from that callback.

When we use default_quick_filters

Changing quick_filters={... to default_quick_filters={

  • user can change Exchange from NYPE to PETX, and the filter does NOT reflects this in the table
  • user can modify any of the filters by clicking on cells and typing. (for example Price or Dollars) Those changes are owned and persisted by the client.

@SimonVutov SimonVutov self-assigned this Jun 26, 2026
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@SimonVutov
SimonVutov force-pushed the feat-DH-21841-fix-user-to-modify branch from 2e9c1ae to 8961441 Compare June 29, 2026 13:05
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@SimonVutov
SimonVutov force-pushed the feat-DH-21841-fix-user-to-modify branch from 8961441 to 8eba401 Compare June 29, 2026 13:11
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@SimonVutov
SimonVutov requested a review from mofojed June 29, 2026 13:15

@mofojed mofojed left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@SimonVutov
SimonVutov requested a review from mofojed June 30, 2026 14:25
@SimonVutov SimonVutov changed the title fix: DH-21841 Allow user to modify table after ui.table sets sort fix: DH-22976 ui.table filters to update when changed programmatically Jun 30, 2026
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

1 similar comment
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@mofojed mofojed left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add e2e test for this guy to make sure of the following:

@SimonVutov
SimonVutov requested a review from mofojed July 2, 2026 13:07
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@SimonVutov

Copy link
Copy Markdown
Contributor Author

Had to re-run the e2e tests a few times.

Summary:

  • Attempt 1 failed with a Chromium ag_grid screenshot mismatch, which looks unrelated to this PR since this PR does not touch the AgGrid test/code.
  • Attempt 2 failed on the new WebKit ui_table persistence test at the screenshot after applying the quick filter. The failure was a screenshot diff.
  • Attempt 3 passed. It still reported a few flaky ui_table screenshot tests, but those passed on retry within the run.

@mofojed mofojed left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@SimonVutov
SimonVutov requested a review from mofojed July 3, 2026 18:58
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@mofojed mofojed left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_filter with 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_changed event so you can process the new filter/apply it in the controlled mode. (This would probably be good to have regardless)

@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@SimonVutov
SimonVutov requested a review from mofojed July 27, 2026 14:28
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

Comment on lines 430 to 459
// 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]
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we don't need it. Thanks for catching that.

@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +418 to +428
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]);
Comment thread tests/ui_table.spec.ts
Comment on lines +133 to +136
// DH-22976: Existing user-owned sorts and quick filters persist after refresh.
test('UI table user sorts and filters persist after refresh', async ({
page,
}) => {
Comment on lines +430 to +434
const hydratedControlledQuickFilters = useMemo(
() =>
hydrateUITableQuickFilters(controlledQuickFilters, model, columns, utils),
[controlledQuickFilters, model, columns, utils]
);
@dsmmcken

Copy link
Copy Markdown
Contributor

So a couple design questions and/or things to consider before merging:

Naming of prop - do we keep quick_filter with 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_changed event so you can process the new filter/apply it in the controlled mode. (This would probably be good to have regardless)

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 mofojed left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
    1. An on_quick_filters_changed handler 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.
    2. A is_quick_filters_read_only to put the quick filters in a state where user cannot edit them (should it just be is_filters_read_only or a general is_read_only instead?).

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.

Comment on lines +204 to +205
quickFilters,
controlledQuickFilters,

@mofojed mofojed Aug 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to controlled filters, we need on_quick_filters_change and on_sorts_change as well.

@mofojed mofojed left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
    1. An on_quick_filters_changed handler 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.
    2. A is_quick_filters_read_only to put the quick filters in a state where user cannot edit them (should it just be is_filters_read_only or a general is_read_only instead?).

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.

@dsmmcken

Copy link
Copy Markdown
Contributor

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.

@SimonVutov
SimonVutov requested a review from mofojed August 13, 2026 21:13
@github-actions

Copy link
Copy Markdown

ui docs preview (Available for 14 days)

@dgodinez-dh

Copy link
Copy Markdown
Contributor

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 margaretkennedy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs changes look fine

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants