feat: add server-side search and pagination for groups page - #27271
feat: add server-side search and pagination for groups page#27271aqandrew wants to merge 23 commits into
Conversation
Add a paginated, server-side searchable groups endpoint and wire the
organization Groups page to it, matching the organization Members page
pattern (useSearchParams + usePaginatedQuery + useFilter with a
presentational view rendering Filter + PaginationContainer).
- database: new PaginatedOrganizationGroups query with COUNT(*) OVER(),
deterministic ORDER BY LOWER(name), OFFSET/LIMIT. dbauthz wrapper does a
single org-wide ActionRead check (no post-filter) to keep LIMIT/OFFSET and
the count consistent, mirroring PaginatedOrganizationMembers.
- codersdk: PaginatedGroupsResponse + GroupsPaginated client method.
- searchquery: minimal Groups(q) parser for free-text search.
- enterprise: paginatedGroups handler + /organizations/{organization}/paginated-groups route.
- frontend: getOrganizationPaginatedGroups API, paginatedGroupsByOrganization
query, GroupsFilter (search-only), GroupsPageView filter + pagination,
updated stories.
The legacy /groups endpoint and its post-filter are left untouched.
Generated with Coder Agents.
Docs previewCheck off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here. |
…onMembersPageView
…oups-search-pagination
Gate the Create group button on the canCreateGroup prop the container already passes, instead of re-deriving permissions via useGroupsSettings + organizationsPermissions inside the view. Calling useGroupsSettings coupled the presentational view to GroupsPageContext, which threw "useGroupsSettings should be used inside of GroupsPageContext" in Storybook.
jeremyruppel
left a comment
There was a problem hiding this comment.
Deep review of the paginated groups endpoint. Clean PR overall: it faithfully mirrors the OrganizationMembers pagination pattern end to end, generalizes Filter cleanly with the presets.length > 0 guard, and the PageBoundaries test is genuinely good coverage.
A few things worth addressing, pinned inline: 6 P2, 4 P3, 3 nits across 13 comments. No P0/P1 and no security holes (auth chain is sound and fail-closed). The two most worthwhile are the non-deterministic pagination ordering and the React Query cache key that group mutations won't invalidate.
Generated by Coder Agents (deep-review), posted on behalf of @jeremyruppel. Severity scale P0 (highest) to P4 (lowest); event is COMMENT only, no approval implied.
| } | ||
|
|
||
| type PaginatedGroupsResponse struct { | ||
| Groups []Group `json:"groups"` |
There was a problem hiding this comment.
if you remove the other type then Groups just needs to be []string
There was a problem hiding this comment.
I'm a little confused by this recommendation. By the other type, do you mean the GroupsParams struct from your other comment? I would think Groups needs to remain []Group regardless, since GroupsPaginated needs to return groups, not just group names. Those group objects get passed into the table in GroupsPageView.tsx:
…oups-search-pagination # Conflicts: # site/src/api/api.ts # site/src/api/queries/groups.ts # site/src/pages/GroupsPage/GroupsPage.tsx # site/src/pages/GroupsPage/GroupsPageView.stories.tsx # site/src/pages/GroupsPage/GroupsPageView.tsx
…-pagination' into aqandrew/devex-434-groups-search-pagination
The name+id tiebreaker commit (7f6e800) accidentally dropped the OFFSET @offset_opt clause when it was collapsed onto the ORDER BY line, which disabled offset pagination and removed OffsetOpt from the generated params. Restore it alongside the id tiebreaker and regenerate.
…nGroupsPaginated Replace UsersRequest with a dedicated PaginatedGroupsRequest (pagination + search only) so the paginated groups client no longer advertises key:value filters the endpoint rejects with a 400. Rename GroupsPaginated to OrganizationGroupsPaginated for parity with OrganizationMembersPaginated and the DB/frontend names. Addresses review comment (codersdk/groups.go).
…upsPaginated The paginated endpoint requires organization-wide group read and is not a drop-in for Groups (GET /groups), which authorizes per-group. Document this on the published SDK method so consumers do not assume equivalent behavior. Addresses review comment (dbauthz.go).
Bare search terms were added as separate 'search' values, so a multi-word query like 'front end' tripped the duplicate-param check and returned a 400 instead of a substring match the SQL supports. Concatenate bare terms into a single search value. Addresses review comment (searchquery/search.go).
PaginatedOrganizationGroups is a :many query that returns an empty slice, never sql.ErrNoRows, so the errors.Is guard was dead code. The empty result is already handled by the len(groups) == 0 branch. Addresses review comment (groups.go).
nickvigilante
left a comment
There was a problem hiding this comment.
Docs updates LGTM!
URL path: /organizations/:organization/groups
The main purpose of this PR is to add a search field and pagination controls to the groups page (DEVEX-434). To be able to add pagination, Coder Agents created a new paginated API endpoint to use when querying groups. All of the Go/SQL code is agent-generated--I gave it as close a look as I could as a frontend developer but I would appreciate some extra scrutiny there. 😅 I can split this into smaller PRs if it's easier
I also moved the "Create group" button down from
GroupsPageintoGroupsPageView, to keep visually consistent with the "Add users" button on the members page.See Storybook for what pagination looks like:
agent output below:
Summary
Adds a server-side, searchable, paginated groups endpoint and wires the
organization Groups page to it. This replaces the previous non-paginated
groupsByOrganizationfetch and matches the established organization Memberspage pattern: a container wires
useSearchParams+usePaginatedQuery+useFilter, and a presentational view renders<Filter>+<PaginationContainer>purely from props.Resolves DEVEX-434.
Changes
Database (OSS
coderd/database/)PaginatedOrganizationGroupsquery: reuses theGetGroupsorg/searchfilters and adds
COUNT(*) OVER(), deterministicORDER BY LOWER(name),and
OFFSET/LIMIT. Regenerated querier/mock/metrics.dbauthzwrapper authorizespolicy.ActionReadonrbac.ResourceGroup.InOrg(orgID)once, with no per-row post-filter,mirroring
PaginatedOrganizationMembers. This is required for SQLLIMIT/OFFSET+COUNT(*) OVER()to stay consistent, and is consistent withthe frontend already gating the whole page on the org-level
viewGroupspermission. The legacy
GetGroupspost-filter is left untouched.codersdk (
codersdk/groups.go)PaginatedGroupsResponse { Groups []Group; Count int }GroupsPaginated(ctx, orgID, UsersRequest)hitting/api/v2/organizations/{org}/paginated-groups.searchquery (
coderd/searchquery/search.go)Groups(q)parser (free-textSearchonly, rejects key:valuefilters via
ErrorExcessParams).Enterprise (
enterprise/coderd/)paginatedGroupshandler enriching each page group with members + totalcount, and the new route registered as a sibling of the existing groups
routes so it inherits the same license/entitlement middleware.
Frontend (
site/src/)getOrganizationPaginatedGroupsAPI + augmented response type keeping theoptional
ai_cost_controlfield.paginatedGroupsByOrganizationpaginated query.GroupsFiltersearch-only filter wrapper.GroupsPagecontainer now usesusePaginatedQuery+useFilter;GroupsPageViewrenders the filter above the table inside aPaginationContainer.stories).
Out of scope
GET /groupsendpoint andgroupsByUserIdInOrganizationstay onthe non-paginated query.
Verification
make gen,make fmt,make lint(via pre-commit) clean.go test ./enterprise/coderd/ -run TestPaginatedGroups(search filtering,page boundaries,
Count) passes.tscclean; biome lint clean.GroupsPageViewstories (12) pass, including the search-fieldand pagination-widget assertions.
Implementation plan (DEVEX-434)
Note: the plan referenced hand-editing
coderd/database/dbmem/dbmem.go, butdbmemhas since been removed from the repo (commit3c2f3d640b), so thatstep was obsolete and skipped; tests run against a real database. The plan's
GroupsPageView.tsxTODO placeholders were also already gone.The endpoint uses an org-wide
ActionReadauthorization check (no post-filter)to keep SQL
LIMIT/OFFSET+COUNT(*) OVER()consistent, mirroringPaginatedOrganizationMembers. See the PR summary for the full breakdown.Generated with Coder Agents.