Skip to content

Commit a3768b5

Browse files
GenerQAQclaude
andauthored
feat(dashboard): unified pagination across all list pages (#451)
* feat(dashboard): unified pagination across all list pages Replace "Load More" buttons with page-number pagination on 6 listing pages (Sessions, Disk, User, Sandbox, Learning Spaces, Agent Skills) and unify the existing Messages/Tasks inline pagination into a shared PaginationBar component. Applied to both commercial and OSS dashboards. - Create shared PaginationBar component for each dashboard - Convert Load More pages to full cursor-loop loading + client pagination - Sticky table headers that stay visible while scrolling - Consistent full-height layout: header fixed, list scrolls, pagination pinned - Add i18n keys for pagination labels (en/zh) - PAGE_SIZE = 20 for listing pages, 10 for detail pages (messages/tasks) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(dashboard): progressive loading — show first page immediately Render the first page of results after a single API call instead of blocking until all cursor pages have been fetched. Remaining pages load in the background while users can already browse loaded data. - Add `isLoading` prop to both PaginationBar components (commercial + OSS) showing a Loader2 spinner next to the item count - Convert all 15 cursor-loop pages to the progressive pattern: fetch first page → render → fetch rest in background - Item count and page buttons update live as more data arrives Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1edc67f commit a3768b5

21 files changed

Lines changed: 886 additions & 684 deletions

File tree

dashboard/app/project/[id]/agent-skills/agent-skills-page-client.tsx

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
ChevronsUpDown,
5858
} from "lucide-react";
5959
import { useTopNavStore } from "@/stores/top-nav";
60+
import { PaginationBar } from "@/components/pagination-bar";
6061
import {
6162
Organization,
6263
Project,
@@ -71,6 +72,8 @@ import {
7172
import { getAllUsers } from "../actions";
7273
import { toast } from "sonner";
7374

75+
const PAGE_SIZE = 20;
76+
7477
interface AgentSkillsPageClientProps {
7578
project: Project;
7679
currentOrganization: Organization;
@@ -90,9 +93,8 @@ export function AgentSkillsPageClient({
9093

9194
const [skills, setSkills] = useState<AgentSkillListItem[]>([]);
9295
const [isLoadingSkills, setIsLoadingSkills] = useState(true);
93-
const [nextCursor, setNextCursor] = useState<string | undefined>(undefined);
94-
const [hasMoreSkills, setHasMoreSkills] = useState(false);
9596
const [isLoadingMore, setIsLoadingMore] = useState(false);
97+
const [currentPage, setCurrentPage] = useState(1);
9698

9799
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
98100
const [skillToDelete, setSkillToDelete] = useState<AgentSkillListItem | null>(null);
@@ -142,35 +144,29 @@ export function AgentSkillsPageClient({
142144
try {
143145
setIsLoadingSkills(true);
144146
const userParam = userFilter === "all" ? undefined : userFilter;
145-
const res = await getAgentSkills(project.id, 50, undefined, true, userParam);
146-
setSkills(res.items || []);
147-
setNextCursor(res.next_cursor);
148-
setHasMoreSkills(res.has_more || false);
147+
148+
const first = await getAgentSkills(project.id, 50, undefined, true, userParam);
149+
setSkills(first.items || []);
150+
setCurrentPage(1);
151+
setIsLoadingSkills(false);
152+
153+
if (first.has_more) {
154+
setIsLoadingMore(true);
155+
let cursor = first.next_cursor;
156+
while (cursor) {
157+
const res = await getAgentSkills(project.id, 50, cursor, true, userParam);
158+
setSkills(prev => [...prev, ...(res.items || [])]);
159+
cursor = res.has_more ? res.next_cursor : undefined;
160+
}
161+
setIsLoadingMore(false);
162+
}
149163
} catch (error) {
150164
console.error("Failed to load skills:", error);
151165
toast.error("Failed to load agent skills");
152-
} finally {
153166
setIsLoadingSkills(false);
154-
}
155-
}, [project.id, userFilter]);
156-
157-
const loadMoreSkills = useCallback(async () => {
158-
if (!nextCursor || isLoadingMore) return;
159-
160-
try {
161-
setIsLoadingMore(true);
162-
const userParam = userFilter === "all" ? undefined : userFilter;
163-
const res = await getAgentSkills(project.id, 50, nextCursor, true, userParam);
164-
setSkills((prev) => [...prev, ...(res.items || [])]);
165-
setNextCursor(res.next_cursor);
166-
setHasMoreSkills(res.has_more || false);
167-
} catch (error) {
168-
console.error("Failed to load more skills:", error);
169-
toast.error("Failed to load more agent skills");
170-
} finally {
171167
setIsLoadingMore(false);
172168
}
173-
}, [project.id, nextCursor, userFilter, isLoadingMore]);
169+
}, [project.id, userFilter]);
174170

175171
const loadUsers = useCallback(async () => {
176172
try {
@@ -195,6 +191,12 @@ export function AgentSkillsPageClient({
195191
)
196192
: skills;
197193

194+
const totalPages = Math.ceil(filteredSkills.length / PAGE_SIZE);
195+
const paginatedSkills = filteredSkills.slice(
196+
(currentPage - 1) * PAGE_SIZE,
197+
currentPage * PAGE_SIZE
198+
);
199+
198200
const handleSkillClick = (skill: SkillItem) => {
199201
const encodedSkillId = encodeId(skill.id);
200202
router.push(`/project/${encodedProjectId}/agent-skills/${encodedSkillId}`);
@@ -409,7 +411,10 @@ export function AgentSkillsPageClient({
409411
type="text"
410412
placeholder="Filter by name..."
411413
value={filterText}
412-
onChange={(e) => setFilterText(e.target.value)}
414+
onChange={(e) => {
415+
setFilterText(e.target.value);
416+
setCurrentPage(1);
417+
}}
413418
className="max-w-sm"
414419
/>
415420
</div>
@@ -438,29 +443,19 @@ export function AgentSkillsPageClient({
438443
) : (
439444
<>
440445
<SkillList
441-
skills={filteredSkills}
446+
skills={paginatedSkills}
442447
onSkillClick={handleSkillClick}
443448
onSkillDelete={handleSkillDelete}
444449
className="overflow-auto flex-1"
445450
/>
446-
{hasMoreSkills && !filterText ? (
447-
<div className="pt-4 flex justify-center shrink-0">
448-
<Button
449-
variant="outline"
450-
onClick={loadMoreSkills}
451-
disabled={isLoadingMore}
452-
>
453-
{isLoadingMore ? (
454-
<>
455-
<Loader2 className="h-4 w-4 animate-spin" />
456-
Loading...
457-
</>
458-
) : (
459-
"Load More"
460-
)}
461-
</Button>
462-
</div>
463-
) : null}
451+
<PaginationBar
452+
currentPage={currentPage}
453+
totalPages={totalPages}
454+
totalItems={filteredSkills.length}
455+
onPageChange={setCurrentPage}
456+
itemLabel="skills"
457+
isLoading={isLoadingMore}
458+
/>
464459
</>
465460
)}
466461
</div>

dashboard/app/project/[id]/disk/disk-page-client.tsx

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
ChevronsUpDown,
6868
MoreHorizontal,
6969
} from "lucide-react";
70+
import { PaginationBar } from "@/components/pagination-bar";
7071
import {
7172
DropdownMenu,
7273
DropdownMenuContent,
@@ -97,6 +98,8 @@ import {
9798
import { getAllUsers } from "../actions";
9899
import { toast } from "sonner";
99100

101+
const PAGE_SIZE = 20;
102+
100103
interface DiskTreeNode extends TreeNode {
101104
path: string;
102105
fileInfo?: Artifact;
@@ -128,9 +131,8 @@ export function DiskPageClient({
128131
const [disks, setDisks] = useState<Disk[]>([]);
129132
const [selectedDisk, setSelectedDisk] = useState<Disk | null>(null);
130133
const [isLoadingDisks, setIsLoadingDisks] = useState(true);
131-
const [nextCursor, setNextCursor] = useState<string | undefined>(undefined);
132-
const [hasMoreDisks, setHasMoreDisks] = useState(false);
133-
const [isLoadingMore, setIsLoadingMore] = useState(false);
134+
const [isLoadingMoreDisks, setIsLoadingMoreDisks] = useState(false);
135+
const [currentPage, setCurrentPage] = useState(1);
134136

135137
// File preview states
136138
const [imageUrl, setImageUrl] = useState<string | null>(null);
@@ -222,39 +224,40 @@ export function DiskPageClient({
222224
disk.id.toLowerCase().includes(filterText.toLowerCase())
223225
);
224226

227+
const totalPages = Math.ceil(filteredDisks.length / PAGE_SIZE);
228+
const paginatedDisks = filteredDisks.slice(
229+
(currentPage - 1) * PAGE_SIZE,
230+
currentPage * PAGE_SIZE
231+
);
232+
225233
const loadDisks = useCallback(async () => {
226234
try {
227235
setIsLoadingDisks(true);
228236
const userParam = userFilter === "all" ? undefined : userFilter;
229-
const res = await getDisks(project.id, 50, undefined, true, userParam);
230-
setDisks(res.items || []);
231-
setNextCursor(res.next_cursor);
232-
setHasMoreDisks(res.has_more || false);
237+
238+
const first = await getDisks(project.id, 50, undefined, true, userParam);
239+
setDisks(first.items || []);
240+
setCurrentPage(1);
241+
setIsLoadingDisks(false);
242+
243+
if (first.has_more) {
244+
setIsLoadingMoreDisks(true);
245+
let cursor = first.next_cursor;
246+
while (cursor) {
247+
const res = await getDisks(project.id, 50, cursor, true, userParam);
248+
setDisks(prev => [...prev, ...(res.items || [])]);
249+
cursor = res.has_more ? res.next_cursor : undefined;
250+
}
251+
setIsLoadingMoreDisks(false);
252+
}
233253
} catch (error) {
234254
console.error("Failed to load disks:", error);
235255
toast.error("Failed to load disks");
236-
} finally {
237256
setIsLoadingDisks(false);
257+
setIsLoadingMoreDisks(false);
238258
}
239259
}, [project.id, userFilter]);
240260

241-
const loadMoreDisks = useCallback(async () => {
242-
if (!nextCursor || isLoadingMore) return;
243-
try {
244-
setIsLoadingMore(true);
245-
const userParam = userFilter === "all" ? undefined : userFilter;
246-
const res = await getDisks(project.id, 50, nextCursor, true, userParam);
247-
setDisks((prev) => [...prev, ...(res.items || [])]);
248-
setNextCursor(res.next_cursor);
249-
setHasMoreDisks(res.has_more || false);
250-
} catch (error) {
251-
console.error("Failed to load more disks:", error);
252-
toast.error("Failed to load more disks");
253-
} finally {
254-
setIsLoadingMore(false);
255-
}
256-
}, [project.id, nextCursor, userFilter, isLoadingMore]);
257-
258261
const loadUsers = useCallback(async () => {
259262
try {
260263
setIsLoadingUsers(true);
@@ -856,12 +859,15 @@ export function DiskPageClient({
856859
type="text"
857860
placeholder="Filter by ID..."
858861
value={filterText}
859-
onChange={(e) => setFilterText(e.target.value)}
862+
onChange={(e) => {
863+
setFilterText(e.target.value);
864+
setCurrentPage(1);
865+
}}
860866
className="w-full"
861867
/>
862868
</div>
863869

864-
<div className="flex-1 overflow-auto">
870+
<div className="flex-1 flex flex-col min-h-0">
865871
{isLoadingDisks ? (
866872
<div className="flex items-center justify-center h-full">
867873
<div className="flex flex-col items-center gap-2">
@@ -877,8 +883,8 @@ export function DiskPageClient({
877883
</div>
878884
) : (
879885
<>
880-
<div className="space-y-2">
881-
{filteredDisks.map((disk) => {
886+
<div className="space-y-2 overflow-auto flex-1">
887+
{paginatedDisks.map((disk) => {
882888
const isSelected = selectedDisk?.id === disk.id;
883889
return (
884890
<div
@@ -925,13 +931,14 @@ export function DiskPageClient({
925931
);
926932
})}
927933
</div>
928-
{hasMoreDisks && !filterText && (
929-
<div className="p-4 flex justify-center">
930-
<Button variant="outline" onClick={loadMoreDisks} disabled={isLoadingMore}>
931-
{isLoadingMore ? (<><Loader2 className="h-4 w-4 animate-spin" />Loading...</>) : "Load More"}
932-
</Button>
933-
</div>
934-
)}
934+
<PaginationBar
935+
currentPage={currentPage}
936+
totalPages={totalPages}
937+
totalItems={filteredDisks.length}
938+
onPageChange={setCurrentPage}
939+
itemLabel="disks"
940+
isLoading={isLoadingMoreDisks}
941+
/>
935942
</>
936943
)}
937944
</div>

0 commit comments

Comments
 (0)