Skip to content

Commit dacc446

Browse files
OAGrclaude
andcommitted
feat(dashboards): add UI pagination to DataTable and fix agent-sessions truncation
Add reusable pagination controls (Prev/Next, page info) to DataTable legacy API. Defaults to 100 rows/page; controls only appear when there are multiple pages. Setting pageSize=0 disables pagination for existing callers that pass large datasets they've already paginated server-side. Also fixes agent-sessions-content.tsx: the hardcoded ?limit=500 fetch of session logs is replaced with fetchAllPaginated, preventing silent truncation when sessions exceed 500 rows. Closes #1717 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dd9e9cc commit dacc446

File tree

2 files changed

+79
-9
lines changed

2 files changed

+79
-9
lines changed

apps/web/src/app/internal/agent-sessions/agent-sessions-content.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { fetchDetailed, withApiFallback, type FetchResult } from "@lib/wiki-server";
2+
import { fetchAllPaginated } from "@lib/fetch-paginated";
23
import { DataSourceBanner } from "@components/internal/DataSourceBanner";
34
import { AgentSessionsTable } from "./sessions-table";
45
import type {
@@ -40,16 +41,18 @@ async function loadFromApi(): Promise<FetchResult<AgentSessionRow[]>> {
4041
);
4142
if (!agentResult.ok) return agentResult;
4243

43-
// Fetch session logs (completed sessions with PR/cost info)
44-
const logsResult = await fetchDetailed<{ sessions: SessionRow[] }>(
45-
"/api/sessions?limit=500",
46-
{ revalidate: 60 }
47-
);
44+
// Fetch all session logs (completed sessions with PR/cost info), paginating through all pages
45+
const logsResult = await fetchAllPaginated<SessionRow>({
46+
path: "/api/sessions",
47+
itemsKey: "sessions",
48+
pageSize: 500,
49+
revalidate: 60,
50+
});
4851

4952
// Build a branch → session log map for enrichment
5053
const logsByBranch = new Map<string, SessionRow>();
5154
if (logsResult.ok) {
52-
for (const log of logsResult.data.sessions) {
55+
for (const log of logsResult.data.items) {
5356
if (log.branch) {
5457
logsByBranch.set(log.branch, log);
5558
}

apps/web/src/components/ui/data-table.tsx

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import {
1212
flexRender,
1313
getCoreRowModel,
1414
getFilteredRowModel,
15+
getPaginationRowModel,
1516
getSortedRowModel,
1617
useReactTable,
1718
} from "@tanstack/react-table"
18-
import { Search } from "lucide-react"
19+
import { Search, ChevronLeft, ChevronRight } from "lucide-react"
1920
import {
2021
Table,
2122
TableBody,
@@ -43,6 +44,8 @@ interface DataTableWithDataProps<TData, TValue> {
4344
defaultSorting?: SortingState
4445
renderExpandedRow?: (row: Row<TData>) => React.ReactNode
4546
getRowClassName?: (row: Row<TData>) => string
47+
/** Number of rows per page. Defaults to 100. Set to 0 to disable pagination. */
48+
pageSize?: number
4649
}
4750

4851
type DataTableProps<TData, TValue = unknown> =
@@ -170,11 +173,23 @@ function DataTableWithData<TData, TValue>({
170173
defaultSorting = [],
171174
renderExpandedRow,
172175
getRowClassName,
176+
pageSize = 100,
173177
}: DataTableWithDataProps<TData, TValue>) {
174178
const [sorting, setSorting] = React.useState<SortingState>(defaultSorting)
175179
const [columnFilters, setColumnFilters] =
176180
React.useState<ColumnFiltersState>([])
177181
const [globalFilter, setGlobalFilter] = React.useState("")
182+
const [pagination, setPagination] = React.useState({
183+
pageIndex: 0,
184+
pageSize: pageSize > 0 ? pageSize : data.length,
185+
})
186+
187+
// Reset to page 1 when filter changes
188+
React.useEffect(() => {
189+
setPagination((p) => ({ ...p, pageIndex: 0 }))
190+
}, [globalFilter])
191+
192+
const paginationEnabled = pageSize > 0
178193

179194
const table = useReactTable({
180195
data,
@@ -184,18 +199,36 @@ function DataTableWithData<TData, TValue>({
184199
getSortedRowModel: getSortedRowModel(),
185200
onColumnFiltersChange: setColumnFilters,
186201
getFilteredRowModel: getFilteredRowModel(),
202+
...(paginationEnabled
203+
? {
204+
getPaginationRowModel: getPaginationRowModel(),
205+
onPaginationChange: setPagination,
206+
}
207+
: {}),
187208
onGlobalFilterChange: setGlobalFilter,
188209
globalFilterFn: "includesString",
189210
state: {
190211
sorting,
191212
columnFilters,
192213
globalFilter,
214+
...(paginationEnabled ? { pagination } : {}),
193215
},
194216
})
195217

218+
const filteredCount = table.getFilteredRowModel().rows.length
219+
const pageCount = table.getPageCount()
220+
const currentPage = table.getState().pagination.pageIndex + 1
221+
const canPrev = table.getCanPreviousPage()
222+
const canNext = table.getCanNextPage()
223+
224+
// Page range info: "Showing 1-100 of 500"
225+
const { pageIndex, pageSize: ps } = table.getState().pagination
226+
const rangeStart = pageIndex * ps + 1
227+
const rangeEnd = Math.min((pageIndex + 1) * ps, filteredCount)
228+
196229
return (
197230
<div className="space-y-4">
198-
{/* Search */}
231+
{/* Search + row count */}
199232
<div className="flex items-center gap-4 pb-4">
200233
<div className="relative flex-1 max-w-md">
201234
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -207,7 +240,9 @@ function DataTableWithData<TData, TValue>({
207240
/>
208241
</div>
209242
<span className="text-sm text-muted-foreground whitespace-nowrap">
210-
{table.getFilteredRowModel().rows.length} of {data.length} results
243+
{filteredCount === data.length
244+
? `${data.length} results`
245+
: `${filteredCount} of ${data.length} results`}
211246
</span>
212247
</div>
213248

@@ -217,6 +252,38 @@ function DataTableWithData<TData, TValue>({
217252
renderExpandedRow={renderExpandedRow}
218253
getRowClassName={getRowClassName}
219254
/>
255+
256+
{/* Pagination controls (only shown when pagination is enabled and there are multiple pages) */}
257+
{paginationEnabled && pageCount > 1 && (
258+
<div className="flex items-center justify-between px-1">
259+
<span className="text-sm text-muted-foreground">
260+
Showing {rangeStart}{rangeEnd} of {filteredCount}
261+
</span>
262+
<div className="flex items-center gap-1">
263+
<button
264+
onClick={() => table.previousPage()}
265+
disabled={!canPrev}
266+
className="inline-flex items-center gap-1 rounded-md border border-border/60 px-2 py-1 text-xs text-muted-foreground hover:bg-muted/50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
267+
aria-label="Previous page"
268+
>
269+
<ChevronLeft className="h-3.5 w-3.5" />
270+
Prev
271+
</button>
272+
<span className="px-2 text-xs text-muted-foreground tabular-nums">
273+
{currentPage} / {pageCount}
274+
</span>
275+
<button
276+
onClick={() => table.nextPage()}
277+
disabled={!canNext}
278+
className="inline-flex items-center gap-1 rounded-md border border-border/60 px-2 py-1 text-xs text-muted-foreground hover:bg-muted/50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
279+
aria-label="Next page"
280+
>
281+
Next
282+
<ChevronRight className="h-3.5 w-3.5" />
283+
</button>
284+
</div>
285+
</div>
286+
)}
220287
</div>
221288
)
222289
}

0 commit comments

Comments
 (0)