Skip to content

Commit d7fca87

Browse files
authored
feat: add pagination to TableView (#83)
1 parent 3d99008 commit d7fca87

2 files changed

Lines changed: 118 additions & 7 deletions

File tree

frontend/src/App.tsx

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ export default function App() {
8686
const [filters, setFilters] = useState<Record<string, string>>({});
8787
const [schemaReloadKey, setSchemaReloadKey] = useState(0);
8888
const [maskSensitive, setMaskSensitive] = useState(false);
89+
const [page, setPage] = useState(0);
90+
const [pageSize] = useState(200);
91+
const [hasNextPage, setHasNextPage] = useState(false);
8992

9093
// Apply theme & mode to <body>
9194
useEffect(() => {
@@ -218,21 +221,25 @@ export default function App() {
218221
sField?: string,
219222
sOrder?: "asc" | "desc",
220223
currentFilters?: Record<string, string>,
224+
targetPage?: number,
221225
) => {
222226
const dbToUse = targetDbId || activeDbId;
223227
if (targetDbId && targetDbId !== activeDbId) {
224228
setActiveDbId(targetDbId);
225229
await loadDatabaseMetadata(targetDbId);
226230
}
227231

232+
const resolvedPage = targetPage ?? 0;
233+
228234
setAppMode("table");
229235
setCurrentTable(name);
230236
setData([]);
231237
setLoading(true);
232238
setError("");
233239

234240
try {
235-
let url = `/api/data/${encodeURIComponent(name)}?dbId=${dbToUse}&limit=200`;
241+
const offset = resolvedPage * pageSize;
242+
let url = `/api/data/${encodeURIComponent(name)}?dbId=${dbToUse}&limit=${pageSize}&offset=${offset}`;
236243
if (sField) {
237244
url += `&sortBy=${encodeURIComponent(sField)}&sortOrder=${sOrder || "asc"}`;
238245
}
@@ -244,8 +251,17 @@ export default function App() {
244251
const payload = await res.json();
245252
if (!res.ok)
246253
throw new Error(payload.error || "Failed to load table data.");
247-
setData(payload.data || []);
248-
showStatus(`Loaded ${(payload.data || []).length} records`);
254+
const rows: Record<string, unknown>[] = payload.data || [];
255+
setData(rows);
256+
setPage(resolvedPage);
257+
setHasNextPage(rows.length === pageSize);
258+
const startRow = offset + 1;
259+
const endRow = offset + rows.length;
260+
showStatus(
261+
rows.length > 0
262+
? `Page ${resolvedPage + 1} · Rows ${startRow}${endRow}`
263+
: "No records found",
264+
);
249265
} catch (err: unknown) {
250266
const msg = err instanceof Error ? err.message : "Unknown error";
251267
showStatus(msg, true);
@@ -254,7 +270,7 @@ export default function App() {
254270
setLoading(false);
255271
}
256272
},
257-
[activeDbId],
273+
[activeDbId, pageSize],
258274
);
259275

260276
const openQueryWorkspace = useCallback(() => {
@@ -280,7 +296,7 @@ export default function App() {
280296
if (appMode === "overview") {
281297
loadOverview();
282298
} else if (currentTable) {
283-
loadTable(currentTable);
299+
loadTable(currentTable, activeDbId, sortBy, sortOrder, filters, 0);
284300
} else if (appMode === "schema") {
285301
setSchemaReloadKey((k) => k + 1);
286302
}
@@ -349,16 +365,36 @@ export default function App() {
349365
sortBy={sortBy}
350366
sortOrder={sortOrder}
351367
filters={filters}
368+
page={page}
369+
pageSize={pageSize}
370+
hasNextPage={hasNextPage}
352371
onSort={(field) => {
353372
const nextOrder =
354373
sortBy === field && sortOrder === "asc" ? "desc" : "asc";
355374
setSortBy(field);
356375
setSortOrder(nextOrder);
357-
loadTable(currentTable, activeDbId, field, nextOrder, filters);
376+
loadTable(currentTable, activeDbId, field, nextOrder, filters, 0);
358377
}}
359378
onFilterChange={(newFilters) => {
360379
setFilters(newFilters);
361-
loadTable(currentTable, activeDbId, sortBy, sortOrder, newFilters);
380+
loadTable(
381+
currentTable,
382+
activeDbId,
383+
sortBy,
384+
sortOrder,
385+
newFilters,
386+
0,
387+
);
388+
}}
389+
onPageChange={(newPage) => {
390+
loadTable(
391+
currentTable,
392+
activeDbId,
393+
sortBy,
394+
sortOrder,
395+
filters,
396+
newPage,
397+
);
362398
}}
363399
/>
364400
);

frontend/src/components/views/TableView.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ interface TableViewProps {
3737
onSort?: (col: string) => void;
3838
onFilterChange?: (filters: Record<string, string>) => void;
3939
maskSensitive?: boolean;
40+
page?: number;
41+
pageSize?: number;
42+
hasNextPage?: boolean;
43+
onPageChange?: (page: number) => void;
4044
}
4145

4246
const escapeHtml = (value: unknown): string => {
@@ -59,6 +63,10 @@ export default function TableView({
5963
onSort,
6064
onFilterChange,
6165
maskSensitive = false,
66+
page = 0,
67+
pageSize = 200,
68+
hasNextPage = false,
69+
onPageChange,
6270
}: TableViewProps) {
6371
const [localFilters, setLocalFilters] =
6472
useState<Record<string, string>>(filters);
@@ -295,6 +303,73 @@ export default function TableView({
295303
</tbody>
296304
</table>
297305
</div>
306+
{/* Pagination footer */}
307+
<div
308+
style={{
309+
display: "flex",
310+
alignItems: "center",
311+
justifyContent: "space-between",
312+
padding: "0.6rem 1rem",
313+
borderTop: "1px solid var(--border)",
314+
background: "var(--surface)",
315+
fontSize: "0.8rem",
316+
color: "var(--text-dim)",
317+
userSelect: "none",
318+
flexShrink: 0,
319+
}}
320+
>
321+
<button
322+
className="icon-btn"
323+
onClick={() => onPageChange?.(page - 1)}
324+
disabled={page === 0}
325+
type="button"
326+
aria-label="Previous page"
327+
style={{
328+
display: "flex",
329+
alignItems: "center",
330+
gap: 6,
331+
padding: "4px 12px",
332+
fontSize: "0.8rem",
333+
opacity: page === 0 ? 0.35 : 1,
334+
cursor: page === 0 ? "not-allowed" : "pointer",
335+
}}
336+
>
337+
← Previous
338+
</button>
339+
340+
<span style={{ fontFamily: "var(--font-mono)", fontSize: "0.78rem" }}>
341+
{rows.length > 0 ? (
342+
<>
343+
Page <strong style={{ color: "var(--text)" }}>{page + 1}</strong>
344+
&nbsp;·&nbsp; Rows{" "}
345+
<strong style={{ color: "var(--text)" }}>
346+
{page * pageSize + 1}{page * pageSize + rows.length}
347+
</strong>
348+
</>
349+
) : (
350+
"No records"
351+
)}
352+
</span>
353+
354+
<button
355+
className="icon-btn"
356+
onClick={() => onPageChange?.(page + 1)}
357+
disabled={!hasNextPage}
358+
type="button"
359+
aria-label="Next page"
360+
style={{
361+
display: "flex",
362+
alignItems: "center",
363+
gap: 6,
364+
padding: "4px 12px",
365+
fontSize: "0.8rem",
366+
opacity: !hasNextPage ? 0.35 : 1,
367+
cursor: !hasNextPage ? "not-allowed" : "pointer",
368+
}}
369+
>
370+
Next →
371+
</button>
372+
</div>
298373
</div>
299374
);
300375
}

0 commit comments

Comments
 (0)