feat(panel): pagination for server lists
This commit is contained in:
6 files changed
+85
-18
No files matched your search
@@ -121,8 +121,8 @@ export function AppShell() {
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1 p-6">
|
||||
<div className="mx-auto max-w-7xl space-y-7">
|
||||
<main className="flex flex-1 flex-col p-6">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex items-center justify-center gap-2 mt-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page <= 1}
|
||||
onClick={() => onChange(page - 1)}
|
||||
>
|
||||
<ChevronLeft />
|
||||
{t("pagination_prev")}
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground min-w-[6rem] text-center">
|
||||
{t("pagination_of", { current: page, total: totalPages })}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => onChange(page + 1)}
|
||||
>
|
||||
{t("pagination_next")}
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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}}"
|
||||
}
|
||||
@@ -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}} 页"
|
||||
}
|
||||
@@ -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() {
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{servers.map((s) => (
|
||||
{paged.map((s) => (
|
||||
<ServerCard key={s.name} server={s} cfg={cfg} onChanged={reload} />
|
||||
))}
|
||||
</div>
|
||||
@@ -51,6 +55,15 @@ export function MyServers() {
|
||||
{t("my_servers_footer")}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{servers.length > 0 && (
|
||||
<Pagination
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={servers.length}
|
||||
onChange={(p) => { setPage(p); if (p > totalPages) setPage(totalPages); }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{servers.map((s) => (
|
||||
{paged.map((s) => (
|
||||
<ServerCard key={s.name} server={s} cfg={cfg} onChanged={reload} />
|
||||
))}
|
||||
</div>
|
||||
@@ -62,6 +60,15 @@ export function ServerAdmin() {
|
||||
{ts("server_admin_footer")}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{servers.length > 0 && (
|
||||
<Pagination
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={servers.length}
|
||||
onChange={(p) => { setPage(p); if (p > totalPages) setPage(totalPages); }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user