+
+
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); }}
+ />
+ )}
>
);
}