From 207bc56dbdc171896d1c0256a67c77a0ee42c627 Mon Sep 17 00:00:00 2001 From: viettranx Date: Thu, 16 Apr 2026 12:30:00 +0700 Subject: [PATCH] feat(ui): persist page size preference across all list pages Add pageSize to useUiStore (persisted in localStorage). Update PAGE_SIZES default to [20, 50, 100, 200]. All 12 paginated pages now read initial value from store and sync changes back globally. --- ui/web/src/components/shared/pagination.tsx | 2 +- ui/web/src/hooks/use-pagination.ts | 9 +++++++-- ui/web/src/pages/activity/activity-page.tsx | 6 +++++- ui/web/src/pages/contacts/contacts-page.tsx | 6 +++++- ui/web/src/pages/memory/memory-page.tsx | 6 +++++- ui/web/src/pages/sessions/sessions-page.tsx | 6 +++++- ui/web/src/pages/traces/traces-page.tsx | 5 ++++- ui/web/src/pages/usage/usage-page.tsx | 8 ++++++-- ui/web/src/stores/use-ui-store.ts | 6 ++++++ 9 files changed, 44 insertions(+), 10 deletions(-) diff --git a/ui/web/src/components/shared/pagination.tsx b/ui/web/src/components/shared/pagination.tsx index cb4a1bf8..0e26e908 100644 --- a/ui/web/src/components/shared/pagination.tsx +++ b/ui/web/src/components/shared/pagination.tsx @@ -9,7 +9,7 @@ import { SelectValue, } from "@/components/ui/select"; -const PAGE_SIZES = [10, 20, 50, 100]; +const PAGE_SIZES = [20, 50, 100, 200]; interface PaginationProps { page: number; diff --git a/ui/web/src/hooks/use-pagination.ts b/ui/web/src/hooks/use-pagination.ts index b867d4b1..04ce074d 100644 --- a/ui/web/src/hooks/use-pagination.ts +++ b/ui/web/src/hooks/use-pagination.ts @@ -1,4 +1,5 @@ import { useState, useMemo, useCallback } from "react"; +import { useUiStore } from "@/stores/use-ui-store"; export interface PaginationState { page: number; @@ -23,8 +24,11 @@ export function usePagination( items: T[], options: UsePaginationOptions = {}, ): UsePaginationReturn { + const globalPageSize = useUiStore((s) => s.pageSize); + const setGlobalPageSize = useUiStore((s) => s.setPageSize); + const [page, setPageRaw] = useState(1); - const [pageSize, setPageSizeRaw] = useState(options.defaultPageSize ?? 20); + const [pageSize, setPageSizeRaw] = useState(options.defaultPageSize ?? globalPageSize); const total = items.length; const totalPages = Math.max(1, Math.ceil(total / pageSize)); @@ -45,7 +49,8 @@ export function usePagination( const setPageSize = useCallback((size: number) => { setPageSizeRaw(size); setPageRaw(1); - }, []); + setGlobalPageSize(size); + }, [setGlobalPageSize]); const resetPage = useCallback(() => setPageRaw(1), []); diff --git a/ui/web/src/pages/activity/activity-page.tsx b/ui/web/src/pages/activity/activity-page.tsx index 32fec416..e01c97ec 100644 --- a/ui/web/src/pages/activity/activity-page.tsx +++ b/ui/web/src/pages/activity/activity-page.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { ClipboardList, RefreshCw } from "lucide-react"; +import { useUiStore } from "@/stores/use-ui-store"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -30,10 +31,13 @@ export function ActivityPage() { const { logs, total, loading, load } = useActivity(); const spinning = useMinLoading(loading); const showSkeleton = useDeferredLoading(loading && logs.length === 0); + const globalPageSize = useUiStore((s) => s.pageSize); + const setGlobalPageSize = useUiStore((s) => s.setPageSize); const [actionFilter, setActionFilter] = useState("all"); const [entityFilter, setEntityFilter] = useState("all"); const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(20); + const [pageSize, setPageSizeRaw] = useState(globalPageSize); + const setPageSize = (size: number) => { setPageSizeRaw(size); setPage(1); setGlobalPageSize(size); }; const totalPages = Math.max(1, Math.ceil(total / pageSize)); diff --git a/ui/web/src/pages/contacts/contacts-page.tsx b/ui/web/src/pages/contacts/contacts-page.tsx index da271d29..318abbf4 100644 --- a/ui/web/src/pages/contacts/contacts-page.tsx +++ b/ui/web/src/pages/contacts/contacts-page.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { ChevronDown, Contact, Info, Merge, RefreshCw, Search, Unlink } from "lucide-react"; +import { useUiStore } from "@/stores/use-ui-store"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -28,12 +29,15 @@ export function ContactsPage() { const { t } = useTranslation("contacts"); const { t: tc } = useTranslation("common"); + const globalPageSize = useUiStore((s) => s.pageSize); + const setGlobalPageSize = useUiStore((s) => s.setPageSize); const [search, setSearch] = useState(""); const [appliedSearch, setAppliedSearch] = useState(""); const [channelType, setChannelType] = useState(""); const [contactType, setContactType] = useState(""); const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(20); + const [pageSize, setPageSizeRaw] = useState(globalPageSize); + const setPageSize = (size: number) => { setPageSizeRaw(size); setPage(1); setGlobalPageSize(size); }; // Selection state const [selectedIds, setSelectedIds] = useState>(new Set()); diff --git a/ui/web/src/pages/memory/memory-page.tsx b/ui/web/src/pages/memory/memory-page.tsx index 79313418..bc57ec6d 100644 --- a/ui/web/src/pages/memory/memory-page.tsx +++ b/ui/web/src/pages/memory/memory-page.tsx @@ -1,6 +1,7 @@ import { useState, useMemo, lazy, Suspense } from "react"; import { useTranslation } from "react-i18next"; import { Plus, RefreshCw, Search, Database } from "lucide-react"; +import { useUiStore } from "@/stores/use-ui-store"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -38,9 +39,12 @@ export function MemoryPage() { const [deleteLoading, setDeleteLoading] = useState(false); const [searchOpen, setSearchOpen] = useState(false); const [indexAllLoading, setIndexAllLoading] = useState(false); + const globalPageSize = useUiStore((s) => s.pageSize); + const setGlobalPageSize = useUiStore((s) => s.setPageSize); const [activeTab, setActiveTab] = useState("documents"); const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(20); + const [pageSize, setPageSizeRaw] = useState(globalPageSize); + const setPageSize = (size: number) => { setPageSizeRaw(size); setPage(1); setGlobalPageSize(size); }; const { documents, diff --git a/ui/web/src/pages/sessions/sessions-page.tsx b/ui/web/src/pages/sessions/sessions-page.tsx index 285e0952..3d0dc851 100644 --- a/ui/web/src/pages/sessions/sessions-page.tsx +++ b/ui/web/src/pages/sessions/sessions-page.tsx @@ -9,6 +9,7 @@ import { Pagination } from "@/components/shared/pagination"; import { Badge } from "@/components/ui/badge"; import { TableSkeleton } from "@/components/shared/loading-skeleton"; import { useDeferredLoading } from "@/hooks/use-deferred-loading"; +import { useUiStore } from "@/stores/use-ui-store"; import { useSessions } from "./hooks/use-sessions"; import { SessionDetailPage } from "./session-detail-page"; import { parseSessionKey } from "@/lib/session-key"; @@ -19,9 +20,12 @@ export function SessionsPage() { const { t } = useTranslation("sessions"); const { key: detailKey } = useParams<{ key: string }>(); const navigate = useNavigate(); + const globalPageSize = useUiStore((s) => s.pageSize); + const setGlobalPageSize = useUiStore((s) => s.setPageSize); const [search, setSearch] = useState(""); const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(20); + const [pageSize, setPageSizeRaw] = useState(globalPageSize); + const setPageSize = (size: number) => { setPageSizeRaw(size); setPage(1); setGlobalPageSize(size); }; const { sessions, total, loading, preview, deleteSession, resetSession, patchSession } = useSessions({ limit: pageSize, diff --git a/ui/web/src/pages/traces/traces-page.tsx b/ui/web/src/pages/traces/traces-page.tsx index c4800f67..15a45a92 100644 --- a/ui/web/src/pages/traces/traces-page.tsx +++ b/ui/web/src/pages/traces/traces-page.tsx @@ -62,11 +62,14 @@ export function TracesPage() { const { t } = useTranslation("traces"); const { t: tc } = useTranslation("common"); const tz = useUiStore((s) => s.timezone); + const globalPageSize = useUiStore((s) => s.pageSize); + const setGlobalPageSize = useUiStore((s) => s.setPageSize); const [agentFilter, setAgentFilter] = useState(); const [channelFilter, setChannelFilter] = useState(); const [selectedTraceId, setSelectedTraceId] = useState(null); const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(20); + const [pageSize, setPageSizeRaw] = useState(globalPageSize); + const setPageSize = (size: number) => { setPageSizeRaw(size); setPage(1); setGlobalPageSize(size); }; const ws = useWs(); const queryClient = useQueryClient(); diff --git a/ui/web/src/pages/usage/usage-page.tsx b/ui/web/src/pages/usage/usage-page.tsx index 8a96dbba..c052175b 100644 --- a/ui/web/src/pages/usage/usage-page.tsx +++ b/ui/web/src/pages/usage/usage-page.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { BarChart3, RefreshCw } from "lucide-react"; +import { useUiStore } from "@/stores/use-ui-store"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { PageHeader } from "@/components/shared/page-header"; @@ -31,9 +32,12 @@ function AnalyticsDashboard() { useUsageAnalytics(filters); // Legacy records table state + const globalPageSize = useUiStore((s) => s.pageSize); + const setGlobalPageSize = useUiStore((s) => s.setPageSize); const { records, total, loading: recLoading, loadRecords } = useUsage(); const [page, setPage] = useState(1); - const [pageSize] = useState(20); + const [pageSize, setPageSizeRaw] = useState(globalPageSize); + const setPageSize = (size: number) => { setPageSizeRaw(size); setPage(1); setGlobalPageSize(size); }; const totalPages = Math.max(1, Math.ceil(total / pageSize)); useEffect(() => { @@ -176,7 +180,7 @@ function AnalyticsDashboard() { total={total} totalPages={totalPages} onPageChange={setPage} - onPageSizeChange={() => {}} + onPageSizeChange={setPageSize} /> )} diff --git a/ui/web/src/stores/use-ui-store.ts b/ui/web/src/stores/use-ui-store.ts index 31b88fdd..35a6f607 100644 --- a/ui/web/src/stores/use-ui-store.ts +++ b/ui/web/src/stores/use-ui-store.ts @@ -11,6 +11,7 @@ interface UiState { timezone: string; // IANA timezone or "auto" sidebarCollapsed: boolean; mobileSidebarOpen: boolean; + pageSize: number; // global pagination page size preference setTheme: (theme: Theme) => void; setLanguage: (language: Language) => void; @@ -18,6 +19,7 @@ interface UiState { toggleSidebar: () => void; setSidebarCollapsed: (collapsed: boolean) => void; setMobileSidebarOpen: (open: boolean) => void; + setPageSize: (size: number) => void; } export const useUiStore = create()( @@ -28,6 +30,7 @@ export const useUiStore = create()( timezone: "auto", sidebarCollapsed: false, mobileSidebarOpen: false, + pageSize: 20, setTheme: (theme) => { set({ theme }); @@ -51,6 +54,8 @@ export const useUiStore = create()( }, setMobileSidebarOpen: (open) => set({ mobileSidebarOpen: open }), + + setPageSize: (size) => set({ pageSize: size }), }), { name: "goclaw:ui", // localStorage key @@ -60,6 +65,7 @@ export const useUiStore = create()( language: state.language, timezone: state.timezone, sidebarCollapsed: state.sidebarCollapsed, + pageSize: state.pageSize, }), } )