refactor(panel): simplify sidebar

This commit is contained in:
Lemon-miaow committed 2026-06-28 00:04:27 +08:00
1 parent b512a18d83
commit a49443cd1f
7 files changed
+51 -167

No files matched your search

+7 -6
View File
@@ -10,10 +10,8 @@ import { Dashboard } from "@/pages/Dashboard";
import { MyServers } from "@/pages/MyServers";
import { ServerConsole } from "@/pages/ServerConsole";
import { Account } from "@/pages/Account";
import { AdminHome } from "@/pages/admin/AdminHome";
import { ServerAdmin } from "@/pages/admin/ServerAdmin";
import { ImageAdmin } from "@/pages/admin/ImageAdmin";
import { OpsOverview } from "@/pages/ops/OpsOverview";
import { FleetTable } from "@/pages/ops/FleetTable";
// Three UX surfaces over two Zero-Trust tiers (DESIGN-WEB-3SIDES):
@@ -45,16 +43,19 @@ export default function App() {
<Route path="servers/:name" element={<ServerConsole />} />
<Route path="account" element={<Account />} />
{/* Admin-Side — admin-tier (server & content ops) */}
{/* Admin-Side — admin-tier (server & content ops).
/admin has no landing page; redirect to the first concrete view
so the section root and any stale bookmarks land somewhere useful. */}
<Route path="admin" element={<RequireAdmin />}>
<Route index element={<AdminHome />} />
<Route index element={<Navigate to="/admin/servers" replace />} />
<Route path="servers" element={<ServerAdmin />} />
<Route path="images" element={<ImageAdmin />} />
</Route>
{/* SysAdmin-Side — admin-tier (platform observability) */}
{/* SysAdmin-Side — admin-tier (platform observability).
Same redirect treatment as /admin — fleet is the meaningful view. */}
<Route path="ops" element={<RequireAdmin />}>
<Route index element={<OpsOverview />} />
<Route index element={<Navigate to="/ops/fleet" replace />} />
<Route path="fleet" element={<FleetTable />} />
</Route>
+43 -28
View File
@@ -5,10 +5,49 @@ import { useTranslation } from "react-i18next";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "@/lib/utils";
import { useTier } from "@/lib/tier";
import { visibleSections } from "@/lib/nav";
import { visibleSections, type NavSection } from "@/lib/nav";
import { Select, SelectContent, SelectItem } from "@/components/ui/select";
import { useTheme } from "@/lib/theme";
function SectionGroup({ section, isFirst }: { section: NavSection; isFirst: boolean }) {
const { t } = useTranslation("navigation");
return (
<div
className={cn(
"flex flex-col gap-0.5",
// Sections after the first get a thin divider with breathing room above,
// which reads as a structural break without needing a loud heading.
!isFirst && "mt-3 border-t border-border/50 pt-3",
)}
>
{section.titleKey && (
<div className="mb-1 px-3 text-[11px] font-medium text-muted-foreground/70">
{t(section.titleKey)}
</div>
)}
{section.items.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.end}
className={({ isActive }) =>
cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
isActive
? "bg-primary/15 text-primary"
: "text-muted-foreground hover:bg-accent hover:text-foreground",
)
}
>
<item.icon className="h-4 w-4" />
{t(item.key)}
</NavLink>
))}
</div>
);
}
function LangToggle() {
const { i18n } = useTranslation();
@@ -67,33 +106,9 @@ export function AppShell() {
<span className="text-lg font-semibold tracking-tight">{t("common:brand_name")}</span>
</div>
<nav className="flex flex-col gap-4">
{sections.map((section) => (
<div key={section.id} className="flex flex-col gap-1">
{section.titleKey && (
<div className="px-3 pb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
{t(section.titleKey)}
</div>
)}
{section.items.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.end}
className={({ isActive }) =>
cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
isActive
? "bg-primary/15 text-primary"
: "text-muted-foreground hover:bg-accent hover:text-foreground",
)
}
>
<item.icon className="h-4 w-4" />
{t(item.key)}
</NavLink>
))}
</div>
<nav className="flex flex-col">
{sections.map((section, i) => (
<SectionGroup key={section.id} section={section} isFirst={i === 0} />
))}
</nav>
@@ -3,10 +3,8 @@
"my_servers": "My servers",
"account": "Account",
"admin_section": "Admin",
"admin_overview": "Overview",
"admin_servers": "Servers",
"admin_images": "Images",
"sysadmin_section": "SysAdmin",
"sysadmin_overview": "Overview",
"sysadmin_fleet": "Fleet"
}
@@ -3,10 +3,8 @@
"my_servers": "我的服务器",
"account": "账户",
"admin_section": "管理",
"admin_overview": "概览",
"admin_servers": "服务器",
"admin_images": "镜像",
"sysadmin_section": "系统管理",
"sysadmin_overview": "概览",
"sysadmin_fleet": "全平台服务器"
}
+1 -5
View File
@@ -2,10 +2,8 @@ import {
LayoutDashboard,
Server,
UserRound,
ShieldCheck,
ServerCog,
Boxes,
Gauge,
Network,
type LucideIcon,
} from "lucide-react";
@@ -31,7 +29,7 @@ export interface NavItem {
export interface NavSection {
id: "user" | "admin" | "ops";
/** Section heading key; null renders no heading (User-Side). */
/** Section heading key; null renders no heading (User-Side flat list). */
titleKey: string | null;
/** When true the section is shown only to admins (is_admin === true). */
adminOnly: boolean;
@@ -54,7 +52,6 @@ export const NAV_SECTIONS: NavSection[] = [
titleKey: "admin_section",
adminOnly: true,
items: [
{ to: "/admin", key: "admin_overview", icon: ShieldCheck, end: true },
{ to: "/admin/servers", key: "admin_servers", icon: ServerCog },
{ to: "/admin/images", key: "admin_images", icon: Boxes },
],
@@ -64,7 +61,6 @@ export const NAV_SECTIONS: NavSection[] = [
titleKey: "sysadmin_section",
adminOnly: true,
items: [
{ to: "/ops", key: "sysadmin_overview", icon: Gauge, end: true },
{ to: "/ops/fleet", key: "sysadmin_fleet", icon: Network },
],
},
-66
View File
@@ -1,66 +0,0 @@
import { Link } from "react-router-dom";
import { ServerCog, Boxes, ShieldCheck } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { useTier } from "@/lib/tier";
// AdminHome is the Admin-Side landing: a short orientation plus links into the
// content/server-admin surfaces. It deliberately states the four-power boundary
// so an admin never expects cluster-lifecycle levers here — those stay kubectl/CRD.
export function AdminHome() {
const { identity } = useTier();
const { t } = useTranslation("admin");
const LINKS = [
{
to: "/admin/servers",
icon: ServerCog,
title: t("servers"),
body: t("servers_desc"),
},
{
to: "/admin/images",
icon: Boxes,
title: t("images"),
body: t("images_desc"),
},
];
return (
<>
<div className="flex items-center gap-3">
<ShieldCheck className="h-6 w-6 text-primary" />
<div>
<h1 className="text-2xl font-semibold tracking-tight">{t("title")}</h1>
<p className="text-sm text-muted-foreground">
{t("subtitle")}
{identity?.email ? t("signed_in_as", { email: identity.email }) : ""}.
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{LINKS.map((l) => (
<Link key={l.to} to={l.to}>
<Card className="h-full transition-colors hover:border-primary/40">
<CardContent className="space-y-2 p-4">
<l.icon className="h-5 w-5 text-primary" />
<div className="font-medium">{l.title}</div>
<p className="text-sm text-muted-foreground">{l.body}</p>
</CardContent>
</Card>
</Link>
))}
</div>
<Card className="border-dashed bg-transparent">
<CardContent className="p-4 text-xs text-muted-foreground">
{t("footer_pre")}
<span className="font-medium">{t("footer_kubectl")}</span>
{t("footer_post")}
</CardContent>
</Card>
</>
);
}
-58
View File
@@ -1,58 +0,0 @@
import { Link } from "react-router-dom";
import { Gauge, Network, ServerOff } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { PendingBackend } from "@/components/States";
// OpsOverview is the SysAdmin-Side landing: a platform-observability cockpit. It is
// admin-tier (same gate as Admin-Side) but a separate *concern* — read-mostly
// visibility across the whole platform, not content/server administration.
//
// The cluster-wide fleet rollup needs a fleet-wide read (GET /admin/servers) that
// does not exist yet — that is a separate Oracle-verifiable backend slice. Until
// then this surface is honest about the gap rather than charting fake data.
export function OpsOverview() {
const { t } = useTranslation("ops");
return (
<>
<div className="flex items-center gap-3">
<Gauge className="h-6 w-6 text-primary" />
<div>
<h1 className="text-2xl font-semibold tracking-tight">{t("title")}</h1>
<p className="text-sm text-muted-foreground">
{t("subtitle")}
</p>
</div>
</div>
<Card>
<CardContent className="space-y-3 p-4">
<div className="flex items-center gap-2 text-sm font-medium">
<Network className="h-4 w-4 text-primary" /> {t("cluster_wide_fleet")}
</div>
<PendingBackend
endpoint="GET /api/v1/admin/servers"
note={t("ops_overview_pending_note")}
/>
<Link
to="/ops/fleet"
className="inline-block text-sm font-medium text-primary hover:underline"
>
{t("open_fleet_table")}
</Link>
</CardContent>
</Card>
<Card className="border-dashed bg-transparent">
<CardContent className="flex items-start gap-3 p-4 text-xs text-muted-foreground">
<ServerOff className="mt-0.5 h-4 w-4 shrink-0" />
<span>
{t("footer_pre")}
<span className="font-medium">{t("footer_kubectl")}</span>
{t("footer_post")}
</span>
</CardContent>
</Card>
</>
);
}