Skip to content

feat(organization): sort, filter and page the org users table - #1832

Draft
dawsontoth wants to merge 1 commit into
stagefrom
claude/1264-org-users-table
Draft

dawsontoth wants to merge 1 commit into
stagefrom
claude/1264-org-users-table

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

⊙ Problem

The organization Users page shows every member in one table with no way to sort it (every column sets enableSorting: false), search it, or page through it (#1264). The table is the shared SimpleBrowseDataTable, which backs ten small, fully-loaded lists; #1268 (instance users) and #1269 (org roles) ask for the same three things next.

❓ Your call: is it warranted now? #1264 is marked v2 ("not likely to be real big in v1 timelines"). Built now because three issues need the same capability and it lives in one component; every other table is untouched unless it opts in, and backing it out is removing two props at a call site.

💡 Solution

SimpleBrowseDataTable gains two opt-in props, and the org users table turns both on:

  • filter={{ label, placeholder }} adds a search box. It drives TanStack's global filter: case-insensitive substring match across every string or number column, except a column that sets enableGlobalFilter: false.
  • paginated pages the rows 20 at a time with the browse table's existing TablePagination. The pager only appears once the rows outnumber the smallest page (20), so a short list looks as it does today.
  • Page rules: a re-sort or a new filter goes back to page 1; a refresh that brings new data keeps the current page, clamped to the last page if the list shrank. TanStack's default resets to page 1 on every new data array, and this list polls every 10s (getOrganizationRolesQueryOptions) while central manager rewrites a member's lastAccessedAt on every GET /User/current (central-manager src/resources/user/User.js:149), so the default would keep bouncing an admin off page 2.

On the org users table every data column is now sortable. Roles becomes an accessor column (the same alphabetical join as before) so it can be sorted and searched, and User Id is left out of the search since an opaque id matches many short queries. The placeholder says what is searched: "Filter by email, name, role or status".

A table that opts into neither prop renders exactly what it did: no search box, no pager, and manualFiltering / manualPagination keep both new row models from running.

⚠️ Look hardest: studioClientTableFeatures in src/lib/table.ts is typed as the shared StudioTableFeatures while carrying two extra runtime slots. That is sound because TanStack's row-model slots are NonFeatureKeys that contribute no types (table-core types/TableFeatures.d.ts:56, destructured out of the features in core/table/constructTable.js:26), and it is needed because TanStack's table types are in out invariant in the feature set, so a distinct type would make every existing ColumnDef / Row unusable here.

⚖️ Alternatives

  • Register the filtered and paginated row models in the shared studioTableFeatures and opt out per table (the manualSorting: true pattern AGENTS.md describes): rejected. Unlike sorting, the paginated row model is never a no-op (its default state is a 10-row page), so it would truncate the browse table (20–250 row server pages), the logs table, DataTable and the seven non-opting SimpleBrowseDataTable lists unless every one of them remembered manualPagination: true.
  • Filter and slice in each page: rejected; three copies of the same state and wiring, and the pager needs the post-filter count only the table has.
  • The planning review (codex) returned better-alternative-exists: also bypass the filtered row model with manualFiltering: !filter for non-opting tables (adopted). Its claim that filterFns: { includesString } must be registered for the filter to work was overruled: the 'auto' global filter resolves filterFn_includesString directly (table-core global-filtering/globalFilteringFeature.utils.js, table_getGlobalAutoFilterFn), and the tests filter through the real row model with no registry.
  • Implementation-review findings not taken: debounce the search box (gemini), since these lists are small and already in memory, and ClustersList filters on every keystroke the same way; the bundle cost of importing TablePagination (codex, then withdrawn by both legs in round 2), since it is already in the eager entry chunk through src/features/instance/routes.ts → databases routes → TableView.

❓ Your call: TablePagination stays in src/features/instance/databases/components/ and the shared component imports it from there (gemini flagged the shared → feature dependency). src/components already imports from @/features in six places. Moving it to src/components/ is a pure rename plus the browse table's import, which I can do here or separately.

❓ Your call: the pager is hidden while every row fits on one 20-row page, so small orgs see no "Page 1 of 1 • N records" bar. Always showing it is a one-line change.

🔧 Changes

✅ Verification

End-to-end route: component integration in jsdom with TanStack's real row models, the real TablePagination and the real org users column definitions. Not browser-verified, since the only authenticated dev origin (port 5173) was shared with other sessions. The authed Playwright spec (e2e/tests/org-users.authed.spec.ts) still asserts the Email/Roles/Status headers, which remain, but it was not run (no test account here) and it does not exercise the new controls.

Stacked follow-ups: #1835 (instance users, #1268) is based on this branch, and #1837 (org roles, #1269) on that one.

Closes #1264

🤖 Generated with Claude Code; posted via @dawsontoth.

Related PRs: #1793 independent (merged; it removed SimpleBrowseDataTable's unused, never-read pagination props, and this adds paging the component holds itself)
Complexity: medium

Review-Coverage: authored=claude; ran=codex,gemini; blocked=cursor-composer(no-receipt),domain(auth),cursor-muse(no-receipt); declined=cursor-grok,cursor-kimi; rounds=2; full=1 @ 28146a1

Review-Attention: read ~5m (raised: degraded review) @ 28146a1

SimpleBrowseDataTable gains two opt-in props: `filter` (a search box
driving TanStack's global filter) and `paginated` (client-side pages of
20, reusing the browse table's TablePagination, shown only once the
rows outnumber one page). The org users table turns both on and makes
every data column sortable, with Roles now an accessor column so it can
be sorted and searched.

A re-sort or a new filter returns to the first page, but new `data`
keeps the current one (clamped to the last page if the list shrank):
these lists poll every 10s, and TanStack's automatic reset would send
the user back to page 1 whenever any record in the payload changed.

The filtered and paginated row models live in a separate
`studioClientTableFeatures` object rather than the shared feature set:
the paginated row model defaults to a 10-row page, so registered
globally it would truncate the browse, logs and every other table that
did not remember `manualPagination: true`. Only the two features' APIs
and state join the shared set, so every table keeps the one table type,
and a SimpleBrowseDataTable that opts into neither prop bypasses both
row models (`manualFiltering` / `manualPagination`).

Refs #1264

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces client-side filtering and pagination to the SimpleBrowseDataTable component, along with comprehensive unit tests and updates to the organization users table to leverage these features. To support this, a new studioClientTableFeatures set is defined to handle client-side row models without impacting other tables. Feedback on these changes suggests moving TablePagination to a shared directory to maintain clean architecture dependencies, and replacing useLayoutEffect with a render-phase state adjustment to avoid SSR warnings.


import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHeader, TableHeadSortable, TableRow } from '@/components/ui/table';
import { PAGE_SIZE_OPTIONS, TablePagination } from '@/features/instance/databases/components/TablePagination';

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.

medium

Importing TablePagination from a feature-specific directory (@/features/instance/databases/components/) into a shared component (@/components/SimpleBrowseDataTable.tsx) violates the clean architecture principle of unidirectional dependencies. Shared components should be self-contained and not depend on feature-specific code.

Consider moving TablePagination.tsx to src/components/ and updating the import.

Suggested change
import { PAGE_SIZE_OPTIONS, TablePagination } from '@/features/instance/databases/components/TablePagination';
import { PAGE_SIZE_OPTIONS, TablePagination } from '@/components/TablePagination';

import { ColumnDef, Row, studioClientTableFeatures } from '@/lib/table';
import { flexRender, PaginationState, RowData, SortingState, useTable } from '@tanstack/react-table';
import { SearchIcon } from 'lucide-react';
import React, { useLayoutEffect, useState } from 'react';

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.

medium

Since useLayoutEffect is being removed to avoid SSR warnings, we can remove it from the React imports as well.

Suggested change
import React, { useLayoutEffect, useState } from 'react';
import React, { useState } from 'react';

Comment on lines +75 to +79
useLayoutEffect(() => {
if (pagination.pageIndex > lastPageIndex) {
setPagination((current) => ({ ...current, pageIndex: lastPageIndex }));
}
}, [pagination.pageIndex, lastPageIndex]);

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.

medium

Using useLayoutEffect in a client component ('use client') will trigger SSR warnings during pre-rendering in frameworks like Next.js (e.g., "useLayoutEffect does nothing on the server...").

Instead of an effect, we can safely adjust the state during the render phase. When React detects a state update during render, it immediately aborts the current render and restarts it with the updated state, avoiding both SSR warnings and layout flashes.

	if (pagination.pageIndex > lastPageIndex) {
		setPagination((current) => ({ ...current, pageIndex: lastPageIndex }));
	}

@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 67.59% 10119 / 14970
🔵 Statements 67.76% 10802 / 15941
🔵 Functions 60.97% 2612 / 4284
🔵 Branches 62.59% 7700 / 12301
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/SimpleBrowseDataTable.tsx 100% 95% 100% 100%
src/features/instance/databases/components/TablePagination.tsx 71.42% 79.24% 66.66% 70.83% 57-58, 96, 119, 232-237, 254, 267, 302-305
src/features/organization/users/index.tsx 0% 0% 0% 0% 21-138
src/features/organization/users/constants/tableDefinition.tsx 100% 75% 100% 100%
src/lib/table.ts 100% 100% 100% 100%
Generated in workflow #2082 for commit 28146a1 by the Vitest Coverage Report Action

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.

Organization(Users) Page v2

1 participant