Generate a complete table listing page with server-side filtering, sorting, and pagination for the Tone frontend. Uses the CustomTable component (TanStack React Table) with ColumnDef-based columns, server-side sorting, sticky columns, infinite scroll or classic pagination. Reads TABLE metadata JSON (from [[backend_tables]]) or inspects the backend API directly to derive column keys, filter options, and sort fields.
Generate a complete table listing page with server-side filtering, sorting, and pagination for the Tone frontend. Uses the CustomTable component (TanStack React Table) with ColumnDef-based columns, server-side sorting, sticky columns, infinite scroll or classic pagination. Reads TABLE metadata JSON (from [[backend_tables]]) or inspects the backend API directly to derive column keys, filter options, and sort fields.
frontend_tables — Table page generator (Tone)
Generates a fully wired table listing page for the Tone frontend. Consumes TABLE metadata JSON produced by [[backend_tables]], or inspects the backend API and models directly when metadata is unavailable. Produces a Next.js page.tsx and any missing React Query hooks needed to power it.
Primary table component:CustomTable from @/components/shared/custom-table — a TanStack React Table wrapper with server-side sorting, sticky columns, infinite scroll, classic pagination, row selection, and loading states.
Hard constraints
Use CustomTable for all new table pages. Do NOT use the legacy DataTable component.
Follow existing patterns exactly. Every generated page must structurally match the conventions in the codebase (see Reference Architecture below).
DO NOT modify shared components (CustomTable, PageHeader, EmptyState, StatusBadge, SelectInput, ConfirmModal). Use them as-is.
DO NOT invent new component abstractions — compose from existing shared components only.
DO NOT add dependencies — use only libraries already in package.json (@tanstack/react-table, lucide-react, sonner, etc.).
DO NOT hardcode data — all data comes from React Query hooks backed by the backend API.
Always verify the backend endpoint exists and its query params before generating filter/sort/pagination logic.
Inputs
Before generating, gather these inputs (ask the user if not provided):
Entity name — e.g. agents, test-runs, evaluators, audit-logs.
Route path — where the page lives under frontend/app/(dashboard)/. Usually matches the entity slug.
Metadata source — one of:
Path to entities.json with a table block from [[backend_tables]].
Or: "inspect backend" — skill reads the backend model + controller directly.
Scope — what features to include: filters, sorting, pagination, selection, bulk-delete, row-click, empty-state. Default: all.
Pagination mode — classic (page buttons) or infinite (scroll to load more). Default: classic.
Reference Architecture
All table pages in this project follow this exact structure. Match it precisely.
Search, filtering, sorting, and pagination are ALL server-side. Every change must update state that flows into the React Query hook params, triggering a new API call. Never filter/sort/search client-side.
exportdefaultfunction <Entity>Page() {
const router = useRouter();
const [page, setPage] = useState(1);
const [sortBy, setSortBy] = useState("-updated_at");
// Search — SearchBar handles debounce internally, onSearch fires the debounced valueconst [search, setSearch] = useState("");
// Filters — one state per filter dropdownconst [statusFilter, setStatusFilter] = useState("");
// Add more as needed: const [typeFilter, setTypeFilter] = useState("");// Named handlers — extract as useCallback, never use inline arrow in JSXconst handleSearch = useCallback((value: string) => {
setSearch(value);
setPage(1);
}, []);
const handleStatusFilter = useCallback((value: string) => {
setStatusFilter(value === "all" ? "" : value);
setPage(1);
}, []);
// Page size selectorconst [pageSize, setPageSize] = useState(10);
const handlePageSizeChange = useCallback((size: number) => {
setPageSize(size);
setPage(1);
}, []);
// ALL params flow into the hook — changes trigger new API request via React Queryconst { data, isLoading } = use<Entity>({
page,
page_size: pageSize,
sort_by: sortBy,
...(search && { search }),
...(statusFilter && { <filter_key>: statusFilter }),
});
const queryClient = useQueryClient();
const [idsToDelete, setIdsToDelete] = useState<string[]>([]);
const [selectedRows, setSelectedRows] = useState<EntityType[]>([]);
const [bulkDeleting, setBulkDeleting] = useState(false);
// Track if filters are active (to distinguish "no data at all" from "no results for filter")const hasActiveFilters = search !== "" || statusFilter !== "";
Search input pattern
Use the SearchBar component from @/components/shared. It handles debounce (400ms default), search icon, and clear button internally.
// In the page JSX:
<div className="flex items-center gap-4">
{/* Search — always first */}
<SearchBar
placeholder="Search <entities>..."
onSearch={handleSearch}
/>
{/* Filter dropdowns — after search, use constants for options */}
<SelectInput
options={<ENTITY>_STATUS_OPTIONS}
value={statusFilter || "all"}
onValueChange={handleStatusFilter}
placeholder="All Status"
className="w-44"
/>
</div>
Search + filter rules:
Search is always present if the backend supports a search body param.
Search is always debounced (400ms) — SearchBar handles this internally.
Filter dropdowns go after the search input in the same row.
Only generate a filter for a field if the backend actually supports it in the body — never send params the API ignores.
Common filterable fields: is_active (boolean), status (enum), type/category (enum), agent_id (reference).
Filter options must be constants — define in frontend/lib/constants/filters.ts as <ENTITY>_<FIELD>_OPTIONS: SelectOption[]. Never inline option arrays in JSX.
Handlers must be named functions — use useCallback for handleSearch, handleStatusFilter, etc. Never use inline arrows in JSX callbacks.
For boolean filters (e.g. is_active), use "active"/"inactive" as select values and convert to true/false when passing to the API: ...(statusFilter && { is_active: statusFilter === "active" }).
Always reset page to 1 when any filter or search changes.
Track hasActiveFilters to distinguish "no data" from "no results for filter" in empty states.
CustomTable usage — Classic pagination
CRITICAL: onSortingChange must always be wired. Without it, clicking sort headers does nothing — the API is never called with the new sort param.
Sorting is always server-side. Every sort column click must update sortBy state, which changes the React Query key, which triggers a new API request. This is the required wiring:
CRITICAL: Bulk delete must call the raw API directly and invalidate queries ONCE after all deletes complete. Never use mutateAsync in a loop — each mutation's onSuccess would trigger a separate list refetch.
Classic pagination with optional page size selector
hasMore
boolean
false
Infinite scroll: more data available
currentPage
number
—
Infinite scroll: current page
totalPages
number
—
Infinite scroll: total pages
loadMore
(page: number) => void
—
Infinite scroll: load next page callback
onRowClick
(row: D) => void
—
Row click handler
enableRowSelection
boolean
false
Enable checkbox row selection
onRowSelectionChange
(rows: D[]) => void
—
Selection change callback
enableSorting
boolean
false
Enable column sort indicators
onSortingChange
(sort: string) => void
—
Sort change callback. Format: "field" (asc) or "-field" (desc)
emptyMessage
string
"No data found"
Empty state text
emptyContent
ReactNode
—
Custom empty state content
containerClassName
string
—
Additional class for outer container
tableClassName
string
—
Additional class for table element
headerClassName
string
—
Additional class for thead tr
rowClassName
string | ((row) => string)
—
Row class (static or dynamic)
cellClassName
string
—
Additional class for all td elements
resetScrollOnDataChange
boolean
true
Reset scroll position on data change
Column meta options
meta: {
stickyLeft?: boolean; // Stick column to left edgestickyRight?: boolean; // Stick column to right edgeclassName?: string; // Applied to both th and tdheaderClassName?: string; // Applied to th onlycellClassName?: string; // Applied to td only
}
Sorting callback format
The onSortingChange callback receives a string:
"name" — sort by name ascending
"-name" — sort by name descending
"-updated_at" — sort by updated_at descending
Pass this directly to the backend API as sort_by or ordering param.
React Query Hook Pattern
If a hook doesn't exist for the entity, generate it in the appropriate file under frontend/lib/api/.
List hook structure
List endpoints use POST to send filter/sort/pagination params in the request body (not query params).
Key difference from GET pattern:api.post("<endpoint>/list", params || {}) sends params as JSON body, not query string. React Query still uses params as the query key for caching/deduplication.
Endpoint derivation
Read the backend router prefix from core/api/v1/<entity_plural>.py.
The frontend API client already prepends /api/v1 via the base URL in frontend/lib/api/client.ts.
List endpoint: "/<entity>/list" (POST) — e.g. "/agents/list", "/test-runs/list".
Other endpoints: "/<entity>" (GET by id), "/<entity>" (POST create), "/<entity>/<id>" (PATCH/DELETE).
Backend Inspection Checklist
When metadata JSON is unavailable, inspect these backend files to derive table configuration:
Model (core/models/<entity>.py):
Column names and types (String, Enum, Boolean, DateTime, ForeignKey, etc.)
Enum/CHECK constraints for filter options
to_dict() method to see the exact response shape
Relationships for reference columns
Controller (core/api/v1/<entity_plural>.py):
List endpoint query params: page, page_size, search, status, agent_id, etc.
Which params actually get used as filters in list_records()
Sort order (the order_by param passed to list_records())
Response format: {"items": [...], "total": N, "page": N, "page_size": N}
Edit navigates to /<entity>/<id>/edit, delete opens ConfirmModal for single item.
Example invocation
User: "Create a table page for test profiles"
Claude:
1. Check if TABLE metadata exists in entities.json for test_profile
2. If not, inspect core/models/test_profile.py and core/api/v1/test_profiles.py
3. Derive columns (ColumnDef[]) from to_dict(), filters from controller query params
4. Check if useTestProfiles hook exists in frontend/lib/api/
5. Check if TestProfile type exists in frontend/types/index.ts
6. Generate page.tsx with CustomTable + ColumnDef columns following Reference Architecture
7. Generate missing hook/type if needed
8. List all files created/modified