From f4df3e20ea12dba8380c6b1cc5eea9c0e45243ce Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sat, 27 Jun 2026 23:37:35 +0800 Subject: [PATCH] feat(panel): pagination for server lists --- panel/src/components/AppShell.tsx | 4 +-- panel/src/components/Pagination.tsx | 41 ++++++++++++++++++++++ panel/src/i18n/resources/en-US/common.json | 5 ++- panel/src/i18n/resources/zh-CN/common.json | 5 ++- panel/src/pages/MyServers.tsx | 21 ++++++++--- panel/src/pages/admin/ServerAdmin.tsx | 27 ++++++++------ 6 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 panel/src/components/Pagination.tsx diff --git a/panel/src/components/AppShell.tsx b/panel/src/components/AppShell.tsx index de5a281..e02d52d 100644 --- a/panel/src/components/AppShell.tsx +++ b/panel/src/components/AppShell.tsx @@ -121,8 +121,8 @@ export function AppShell() { -
-
+
+
diff --git a/panel/src/components/Pagination.tsx b/panel/src/components/Pagination.tsx new file mode 100644 index 0000000..ba6ddb9 --- /dev/null +++ b/panel/src/components/Pagination.tsx @@ -0,0 +1,41 @@ +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { Button } from "@/components/ui/button"; + +interface Props { + page: number; + pageSize: number; + total: number; + onChange: (page: number) => void; +} + +export function Pagination({ page, pageSize, total, onChange }: Props) { + const { t } = useTranslation("common"); + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + + return ( +
+ + + {t("pagination_of", { current: page, total: totalPages })} + + +
+ ); +} diff --git a/panel/src/i18n/resources/en-US/common.json b/panel/src/i18n/resources/en-US/common.json index e2140d7..db6bed8 100644 --- a/panel/src/i18n/resources/en-US/common.json +++ b/panel/src/i18n/resources/en-US/common.json @@ -19,5 +19,8 @@ "lang_en": "EN", "lang_zh": "中文", "lang_toggle_hint": "Switch to {{lang}}", - "toggle_theme": "Toggle theme" + "toggle_theme": "Toggle theme", + "pagination_prev": "Previous", + "pagination_next": "Next", + "pagination_of": "Page {{current}} of {{total}}" } diff --git a/panel/src/i18n/resources/zh-CN/common.json b/panel/src/i18n/resources/zh-CN/common.json index 540c6b6..fdc51f9 100644 --- a/panel/src/i18n/resources/zh-CN/common.json +++ b/panel/src/i18n/resources/zh-CN/common.json @@ -19,5 +19,8 @@ "lang_en": "EN", "lang_zh": "中文", "lang_toggle_hint": "切换至 {{lang}}", - "toggle_theme": "切换主题" + "toggle_theme": "切换主题", + "pagination_prev": "上一页", + "pagination_next": "下一页", + "pagination_of": "第 {{current}} / {{total}} 页" } diff --git a/panel/src/pages/MyServers.tsx b/panel/src/pages/MyServers.tsx index 2ca3677..5f9ce54 100644 --- a/panel/src/pages/MyServers.tsx +++ b/panel/src/pages/MyServers.tsx @@ -1,19 +1,23 @@ +import { useState } from "react"; import { Server } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Card, CardContent } from "@/components/ui/card"; import { ServerCard } from "@/components/ServerCard"; +import { Pagination } from "@/components/Pagination"; import { Loading, ErrorState, EmptyState } from "@/components/States"; import { api } from "@/lib/api"; import { useAsync, useConfig } from "@/lib/hooks"; -// MyServers is the User-Side fleet: list / wake / stop / claim the servers the -// caller owns or may claim. Creation lives on Admin-Side now (POST /servers is -// admin-tier) — the platform provisions servers; users claim and operate them. +const PAGE_SIZE = 12; + export function MyServers() { const cfg = useConfig(); const { data, error, loading, reload } = useAsync(() => api.myServers(), []); const { t } = useTranslation("servers"); const servers = data ?? []; + const [page, setPage] = useState(1); + const totalPages = Math.max(1, Math.ceil(servers.length / PAGE_SIZE)); + const paged = servers.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); return ( <> @@ -40,7 +44,7 @@ export function MyServers() { /> ) : (
- {servers.map((s) => ( + {paged.map((s) => ( ))}
@@ -51,6 +55,15 @@ export function MyServers() { {t("my_servers_footer")} + + {servers.length > 0 && ( + { setPage(p); if (p > totalPages) setPage(totalPages); }} + /> + )} ); } diff --git a/panel/src/pages/admin/ServerAdmin.tsx b/panel/src/pages/admin/ServerAdmin.tsx index 4f8f29d..1d46c27 100644 --- a/panel/src/pages/admin/ServerAdmin.tsx +++ b/panel/src/pages/admin/ServerAdmin.tsx @@ -1,27 +1,25 @@ +import { useState } from "react"; import { ServerCog } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Card, CardContent } from "@/components/ui/card"; import { ServerCard } from "@/components/ServerCard"; +import { Pagination } from "@/components/Pagination"; import { CreateServerDialog } from "@/components/CreateServerDialog"; import { Loading, ErrorState, EmptyState } from "@/components/States"; import { api } from "@/lib/api"; import { useAsync, useConfig } from "@/lib/hooks"; -// ServerAdmin is the Admin-Side create surface. CreateServerDialog lives here (it -// calls the admin-tier POST /servers, spec §15) rather than on User-Side, where it -// previously rendered for everyone and relied on a 403 — moving it removes that -// UX/tier mismatch. -// -// The list below currently uses the app-tier GET /me/servers, i.e. the servers -// THIS admin owns or may manage. A fleet-wide GET /admin/servers (every server, -// not just mine) is a separate Oracle-verifiable backend slice that powers the -// SysAdmin-Side FleetTable; it is intentionally not faked in here. +const PAGE_SIZE = 12; + export function ServerAdmin() { const cfg = useConfig(); const { data, error, loading, reload } = useAsync(() => api.myServers(), []); const { t } = useTranslation("admin"); const { t: ts } = useTranslation("servers"); const servers = data ?? []; + const [page, setPage] = useState(1); + const totalPages = Math.max(1, Math.ceil(servers.length / PAGE_SIZE)); + const paged = servers.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); return ( <> @@ -51,7 +49,7 @@ export function ServerAdmin() { /> ) : (
- {servers.map((s) => ( + {paged.map((s) => ( ))}
@@ -62,6 +60,15 @@ export function ServerAdmin() { {ts("server_admin_footer")} + + {servers.length > 0 && ( + { setPage(p); if (p > totalPages) setPage(totalPages); }} + /> + )} ); }