Fix pagination footer ignoring the active list filter - #85
Merged
Conversation
The CRUD list pages never sent the search box term to the backend. Every list query fetched an unfiltered page and `DataTable` narrowed the rows in the browser, so the footer printed the unfiltered `total` next to a handful of visible rows — "350 rows... Page 1 of 18" above a single matching entity. Paging was broken the same way: the search only ever matched rows that happened to be on the page already on screen. Forward the search term as `q` on the list queries whose resolver accepts it (tenants, entities, groups, resources, roles) so the backend narrows the rows and `total` together and the filtered set pages properly. Tell the table which filters were resolved server-side; it skips re-filtering those and, for the resources with no backend text search, counts the rows it filtered itself rather than reporting a total that no longer describes them. Also clamp the displayed page to the available range so a collapsed result set cannot render "Page 7 of 1". Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com>
drasko
pushed a commit
that referenced
this pull request
Aug 17, 2026
* Add attributesContains filter to entities and groups queries (#63) * Add objectId reverse lookup to directPolicies query (#64) * Make object group membership many-to-many (#65) * Add external_id to entities, unique per tenant (#66) * Expose scoping filters on authorizedObjectIds (#68) * Fix blank externalId filter and no-op object-group removal events (#77) * Keep a blank externalId filter distinct from an omitted one normalize_external_id collapsed both "filter omitted" and "filter explicitly blank/whitespace" to None. The SQL callers bind that as `col IS NULL OR col = $n`, so a caller who passed a blank externalId filter (a mistake, or an attempt to match nothing) got back the full unfiltered authorized/listed set instead of zero rows. Every Some(_) input now stays Some(_): blank, NUL-bearing, and over-cap values normalize to a sentinel longer than MAX_EXTERNAL_ID_LEN, which the length CHECK constraint guarantees no stored row can equal. Only a genuinely omitted None still means "no filter". Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * Suppress object-group membership events on no-op removals remove_entity_from_object_group, clear_entity_object_groups, and their resource counterparts discarded the DELETE's affected-row count and always published entity.object_group.remove / entity.object_groups.clear (and the resource equivalents), even when zero rows were deleted — e.g. removing a membership that never existed, or clearing an already-empty set. Event consumers were told a change happened when nothing changed. delete_entity_object_groups_in_tx and delete_resource_object_groups_in_tx now return the deleted row count, and the four *_with_audit callers skip the event publish (but still commit) when it's zero — the same shape already used by add_entity_to_object_group_with_audit / add_resource_to_object_group_with_audit for the additive side, and by remove_group_member_with_audit for principal-group membership. Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --------- Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * Surface object-group membership and reverse policy lookup in the UI (#82) * Surface object-group membership and reverse policy lookup in the UI ATOM-04 (many-to-many object-group membership) and ATOM-03 (directPolicies(objectId:) reverse lookup) both shipped backend-only. The admin panel never grew a way to reach either, so object groups had no membership UI at all and there was no way to ask who an entity or resource is shared with. This closes that gap against the existing schema; no resolver changes. Object groups get a Members panel in the inspect sheet, mirroring the principal-group branch rather than replacing it. There is no "members of this object group" query, so it reads the list filters that ATOM-04 made work — entities(parentGroupId:) and resources(parentGroupId:) — under an Entities/Resources tab pair, each with a tenant-scoped search-to-add picker and a per-row remove. The same membership is editable from the other side: entity and resource details now carry an Object groups section built from the objectGroupIds the list query already selects, so opening the sheet costs no extra round trip. Names resolve through the existing useNameMap. Groups are picked from objectGroups(...) rather than groups(...) so principal groups can never appear. clearEntityObjectGroups / clearResourceObjectGroups are exposed separately, behind a confirmation, since they drop every membership at once. Both directions share lib/object-groups: one set of documents and one mutation hook, so the two views cannot drift. The mutations return the mutated object, so its objectGroupIds seed the cache directly instead of forcing a refetch. Adds and removes are idempotent server-side (ON CONFLICT DO NOTHING, zero-row delete), so there is deliberately no client-side "already a member" guard to go stale. Entities and resources also gain a read-only Access tab over directPolicies(objectId:, objectKind:). Per the resolver's own doc comment this lists only policies whose permission block names the object — role-derived access and platform/tenant/kind-wide grants are excluded — so each row says why it reaches this object. Creating a grant still belongs to the Policies page. Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * fix(ui): hide object group add picker without tenant scope Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --------- Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * Hide already-joined items from object-group add pickers (#84) * Hide already-joined items from object-group add pickers The four search-to-add pickers added with the object-group membership UI offered every candidate in the tenant, including the ones already joined. Adding one of those is a server-side no-op, so the picker showed an Add button that changed nothing and still reported success — and nothing in the row said the item was already a member. Both directions now filter on `objectGroupIds`: the entity/resource side already holds its own membership set, and the group-side candidates query selects each candidate's set, so neither picker needs an extra round trip. The candidate cache is invalidated alongside the members cache, since an add or remove is exactly what makes those rows stale. When filtering empties a non-empty result, the picker says every match is already joined rather than claiming there is nothing to find. Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * Fix object group picker pagination for joined members Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * Fix object group picker pagination for joined groups Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --------- Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * Fix pagination footer ignoring the active list filter (#85) The CRUD list pages never sent the search box term to the backend. Every list query fetched an unfiltered page and `DataTable` narrowed the rows in the browser, so the footer printed the unfiltered `total` next to a handful of visible rows — "350 rows... Page 1 of 18" above a single matching entity. Paging was broken the same way: the search only ever matched rows that happened to be on the page already on screen. Forward the search term as `q` on the list queries whose resolver accepts it (tenants, entities, groups, resources, roles) so the backend narrows the rows and `total` together and the filtered set pages properly. Tell the table which filters were resolved server-side; it skips re-filtering those and, for the resources with no backend text search, counts the rows it filtered itself rather than reporting a total that no longer describes them. Also clamp the displayed page to the available range so a collapsed result set cannot render "Page 7 of 1". Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --------- Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On every CRUD list page, typing into the filter box narrowed the displayed rows but left the pagination footer showing the unfiltered count.
Searching
/entitiesfor a term matching one entity showed a single row above a footer reading350 rows … Page 1 of 18./groupsbehaved identically (94 rows … Page 1 of 5).Root cause
The search box was never wired to the backend. It writes
<resource>.qinto the URL, butCrudWorkspaceonly forwardedresource.filtersentries andstatusto GraphQL — neverq. So the list query always fetched an unfiltered page, andDataTablenarrowed those rows in the browser:The footer then printed the server's
total, which still counted the whole unfiltered set. Rows and count came from two different notions of "the result set".This also meant search only ever matched rows that were already on the visible page — searching for something on page 5 while viewing page 1 found nothing.
Fix
qon the list queries whose resolver accepts it —tenants,entities,groups,resources,roles— so the backend narrows rows andtotaltogether and the filtered set pages correctly.serverFiltersdown toDataTableso it knows which filters were already resolved server-side. It skips re-filtering those (re-applying the browser's broaderJSON.stringifymatch would drop valid backend matches).profiles,permissionBlocks,actions,actionApplicability,actionAssignmentRules,directPolicies) the client-side fallback stays, but the footer now counts the rows it actually filtered instead of reporting a total that no longer describes them.Page 7 of 1.Fixed in the one shared component, so it applies to every list page.
Scope note
This bug predates #69 and #82 — neither caused it. It was found while QA-testing their UI, not introduced by them. #69 is Rust/GraphQL backend only, and #82 touched only the inspect sheets,
object-group-*,object-access-panel.tsx,lib/access/andlib/object-groups/— none of the shared list/table/pagination plumbing. The client-side filter block responsible dates back to40e1512"Add initial admin UI (#1)".Test plan
Manual (browser, dev UI on
:3012against the running backend on:8080, ~350 entities / 94 groups)Before the fix:
/entitiesqa-test-entity-3350 rows,Page 1 of 18/groupsqa-test-group-294 rows,Page 1 of 5After the fix:
/entitiesqa-test-entity-31 row,Page 1 of 1/groupsqa-test-group-21 row,Page 1 of 1Regression checks, all after the fix:
/entities350 rows / Page 1 of 18,/groups94 rows / Page 1 of 5, matching the pre-fix unfiltered baseline./entities?entities.page=2→Page 2 of 18, 20 different rows, all four page links enabled./entities?entities.q=tenant&entities.limit=10→12 rows / Page 1 of 2, 10 rows, Next preservesq; page 2 →Page 2 of 2, remaining 2 rows, search box still showstenant, Next/Last correctly disabled./groups; returned to94 rows / Page 1 of 5with paging re-enabled.$qresources —/tenants?q=demo2 rows,/roles?q=admin57 → 6 rows,/resources?q=telemetry1 row. NoBackend unavailablealerts./profiles?profiles.q=Gateway(no backendq) still filters to 1 row and the footer reports1 row.Automated
pnpm lint— clean (178 files).pnpm test— 25 files, 103 tests passing.pnpm build— compiles successfully.Added 5 tests to
components/ui/data-table.test.tsx. Verified they are real regression tests: revertingdata-table.tsxto its pre-fix state fails 4 of them with exactly the reported symptom (expected 'ViewNameneedle350 rows…' to contain '1 row'), and they pass with the fix.