FE-840: Add Property Filter To Entities Visualizer#8786
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
🤖 Augment PR SummarySummary: This PR adds per-property value filtering to the Entities Visualizer so users can narrow the table by property values (e.g. numeric comparisons, string matching, boolean/existence checks). Changes:
Technical Notes: Property filter clauses are ANDed with existing web/type/archived filters, and operator/value input is debounced to reduce refetch churn while typing. 🤖 Was this summary useful? React with 👍 or 👎 |
09f3d64 to
477f2ea
Compare
PR SummaryMedium Risk Overview Ribbon UI adds a searchable Property… flow ( Table stability: Cleanup: visualizer modules move under Reviewed by Cursor Bugbot for commit 178deff. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 178deff. Configure here.
| {operatorDescriptor.label} | ||
| </MenuItem> | ||
| ))} | ||
| </Select> |
There was a problem hiding this comment.
Select closes popover cancels draft
High Severity
The property-filter popover’s onClose handler treats every close as cancel in add mode and calls onRemove, which drops the draft pill. The operator Select uses the default portaled menu, so choosing an operator is often treated as an outside click and closes the popover before the user can enter a value or click Add—especially for string and number filters where the default operator is not enough on its own.
Reviewed by Cursor Bugbot for commit 178deff. Configure here.
| kind: property.kind, | ||
| operator: getDefaultOperatorForKind(property.kind), | ||
| }); | ||
| }; |
There was a problem hiding this comment.
New property replaces existing draft
Low Severity
Starting a second property filter from the add menu calls setDraftPropertyFilter with a new draft and overwrites any existing draft. There is no guard on the Property menu item or merge step, so an in-progress pill (including a partially filled value) is dropped silently when another property is chosen.
Reviewed by Cursor Bugbot for commit 178deff. Configure here.
TimDiekmann
left a comment
There was a problem hiding this comment.
Given how many edge cases we have here (and I really don't like the UX, from a filter I expect it to behave differently): I would not filter the types based on the currently shown entities.
| const setPropertyFilters = ( | ||
| updater: (prev: PropertyFilter[]) => PropertyFilter[], | ||
| ) => | ||
| setFilterState((prev) => ({ | ||
| ...prev, | ||
| propertyFilters: updater(prev.propertyFilters), | ||
| })); |
There was a problem hiding this comment.
Just a word of warning: Without a web-filter and without a type filter, this will slow down the frontend like crazy for big graphs. Also for web filters with webs with very many entities. Properties are not indexed (due to the nature of JSONb in Postgres). The single archived property before already implied this effect as Postgres requires to do a full scan. We need to be careful with adding filters to random things.
| * **filterable** interpretation wins, so the user can still filter on the column | ||
| * they see. Among unfilterable interpretations the first encountered wins. | ||
| * | ||
| * Pure function – authored so it can be unit-tested in isolation. |
There was a problem hiding this comment.
It would be good to also add a few unit tests in this case
| default: | ||
| return null; |
There was a problem hiding this comment.
If we add a new operator, we very likely miss to add It here. It would be good if the switch statement would be exhaustive (and add satisfies never)
| if (typeIds.length === 0) { | ||
| return { | ||
| clause: { | ||
| equal: [{ path: ["type", "versionedUrl"] }, { parameter: "" }], |
There was a problem hiding this comment.
This is not a valid filter. An always-true filter is all: [] ("every") and an always-false filter is any: [] ("at least one").
| const filterStateWithoutType = useMemo<EntitiesFilterState>( | ||
| () => ({ | ||
| ...filterState, | ||
| type: { selectedTypeIds: null }, | ||
| }), | ||
| [filterState], | ||
| ); |
There was a problem hiding this comment.
This only nulls the type, but it retains the property filters. This means other types cannot be selected anymore. It also implies the quite expensive call with includeTypeIds. I would not restrict the type IDs at all. If you search for a type, e.g. Pdf, but don't have any in your web, the result set is empty. That's expected UX.
| mode: "add" | "edit"; | ||
| /** Whether to open the editor automatically (just-added draft). */ | ||
| autoOpen: boolean; | ||
| onAutoOpenHandled: () => void; |
| filter: PropertyFilter; | ||
| mode: "add" | "edit"; | ||
| /** Whether to open the editor automatically (just-added draft). */ | ||
| autoOpen: boolean; |
| baseUrl: BaseUrl; | ||
| title: string; | ||
| kind: FilterValueKind; | ||
| }) => void; |
| const handleAddPropertyFilter = (property: { | ||
| baseUrl: BaseUrl; | ||
| title: string; | ||
| kind: FilterValueKind; | ||
| }) => { |
| {filterState.propertyFilters.map((propertyFilter) => ( | ||
| <PropertyFilterPill | ||
| key={propertyFilter.id} | ||
| filter={propertyFilter} | ||
| mode="edit" | ||
| autoOpen={false} | ||
| onAutoOpenHandled={() => {}} | ||
| onCommit={(committed) => | ||
| handleCommitPropertyFilter(propertyFilter.id, committed) | ||
| } | ||
| onRemove={() => handleRemovePropertyFilter(propertyFilter.id)} | ||
| /> | ||
| ))} | ||
| {draftPropertyFilter && ( | ||
| <PropertyFilterPill | ||
| key={draftPropertyFilter.id} | ||
| filter={draftPropertyFilter} | ||
| mode="add" | ||
| autoOpen | ||
| onAutoOpenHandled={() => {}} | ||
| onCommit={handleCommitDraftPropertyFilter} | ||
| onRemove={() => setDraftPropertyFilter(null)} | ||
| /> |
There was a problem hiding this comment.
It's not clear, what filters apply how
The property filter could mean they both needs to match or any needs to match
I appreciate, that we don't have a way to create other filters (such as (X and Y) or (A and B) but it should be clear from the UI what is filtered.
Also, it's not possible to only see archived entities.


🌟 What is the purpose of this PR?
Follow-up for #8783 based on the task
Adds per-property value filtering to the Entities Visualizer. Previously the ribbon only supported filtering by web, type, and archived status; you could narrow the table by which entities were shown but not by the values of their properties.
This lets a user filter the table on the properties they can see — e.g.
Age > 13,Name contains "Acme",Active is true, orEmail has any value— by adding property-filter "pills" to the existing filter ribbon. Each pill picks a property, an operator appropriate to the property's value kind, and (where relevant) a value; the resulting clauses are ANDed with the existing web / type / archived filters and pushed down into the graph query.🔍 What does this change?
Filter model & query building
EntitiesFilterStatewith apropertyFiltersarray (data/types.ts).data/property-filters/types.tsdefiningPropertyFilter, thePropertyFilterOperatorset, theFilterValueKind(number/string/boolean), and theFilterablePropertyshape (filterable, or disabled-with-reason).buildPropertyFilterClause(data/property-filters/build-property-filter-clause.ts) translates a single filter into a graphFilterclause as a pure, unit-testable function. It owns parameter typing (numbers coerced to JS numbers so comparisons are numeric, strings kept untrimmed for case-sensitive matching) and theexists/not existscomposition. An incomplete or invalid filter yieldsnulland contributes no clause, so an unfinished pill is inert rather than matching nothing.buildEntitiesFilternow folds these clauses into the query.deriveFilterableProperties(data/property-filters/derive-filterable-properties.ts) derives the pickable properties from the same closed-entity-type / definitions data that builds the visible columns ("filter on the columns you see"). Properties that can't be filtered in v1 (lists, nested objects, multiple data types, unsupported kinds) are either omitted or surfaced disabled with a reason. The filterable interpretation wins when a base URL appears in multiple shapes across types.get-operators-for-kind.tscatalogs the operators available per value kind, their dropdown labels, pill connectors, and whether they require a value, with a sensible default operator per kind.UI
AddFiltersMenugains a "Property…" entry that opens an in-menuPropertyFilterPicker(searchable list of filterable properties, disabled rows with explanatory tooltips).PropertyFilterPillrenders each property filter in the ribbon with a kind icon, operator/value editor popover (operator changes apply immediately; value input is debounced before hitting shared state to avoid refetching on every keystroke), an active/incomplete visual state, and removal.FilterRibbonwires up add/change/remove handlers, auto-opens the editor for a just-added pill, wraps to multiple lines, and includes property filters in the "filters are default" / clear-all logic.incompletePillSxadded for the muted placeholder look of pills that don't yet contribute a clause.Data-type pool robustness (supporting change)
EntitiesTableDatanow bundles thedataTypeDefinitionspool alongside the rows it was generated from, unioned across paginated pages. This guarantees a row's value can always resolve its data type and fixesformatValuethrowing when a refetch narrowed the result set (e.g. a property filter matching nothing) before rows were regenerated.EntitiesTableno longer takesdefinitionsdirectly and reads the pool from the table data.getReferencedDataTypeIds(format-value.ts) is exposed so the table can cheaply check that every data type a value depends on is present before rendering, falling back to a "Not Found" cell instead of crashing the grid.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
number,string, andbooleanproperties. List, nested, and multiple-data-type properties are listed in the picker but disabled with an explanatory tooltip.equals/contains/startsWith/endsWith) is case-sensitive.🐾 Next steps
🛡 What tests cover this?
❓ How to test this?
📹 Demo
Screenshare.-.2026-05-29.2_10_00.PM.mp4