feat(panel): i18n
This commit is contained in:
47 files changed
+898
-231
No files matched your search
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
|
||||
@@ -7,33 +8,34 @@ import { useTier } from "@/lib/tier";
|
||||
// content/server-admin surfaces. It deliberately states the four-power boundary
|
||||
// so an admin never expects cluster-lifecycle levers here — those stay kubectl/CRD.
|
||||
|
||||
const LINKS = [
|
||||
{
|
||||
to: "/admin/servers",
|
||||
icon: ServerCog,
|
||||
title: "Servers",
|
||||
body: "Create platform servers from the structured §15 form and manage the ones you operate.",
|
||||
},
|
||||
{
|
||||
to: "/admin/images",
|
||||
icon: Boxes,
|
||||
title: "Images",
|
||||
body: "The platform image whitelist — the value space the create form draws from.",
|
||||
},
|
||||
];
|
||||
|
||||
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="mx-auto max-w-4xl space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<ShieldCheck className="h-6 w-6 text-primary" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Admin</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{t("title")}</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Server & content administration
|
||||
{identity?.email ? ` · signed in as ${identity.email}` : ""}.
|
||||
{t("subtitle")}
|
||||
{identity?.email ? t("signed_in_as", { email: identity.email }) : ""}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,11 +56,9 @@ export function AdminHome() {
|
||||
|
||||
<Card className="border-dashed bg-transparent">
|
||||
<CardContent className="p-4 text-xs text-muted-foreground">
|
||||
Cluster scaling, RBAC, Secrets and control-plane lifecycle are{" "}
|
||||
<span className="font-medium">kubectl / CRD operations</span> and are
|
||||
intentionally not available from the panel — the four-power separation
|
||||
(build / runtime / operator / app) is preserved. This is a window onto the
|
||||
platform, not a lever for operator power.
|
||||
{t("footer_pre")}
|
||||
<span className="font-medium">{t("footer_kubectl")}</span>
|
||||
{t("footer_post")}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Boxes, CheckCircle2, CircleSlash } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Loading, ErrorState, EmptyState } from "@/components/States";
|
||||
@@ -11,17 +12,16 @@ import { useAsync } from "@/lib/hooks";
|
||||
// added, so this page surfaces the whitelist honestly and labels the missing
|
||||
// mutation rather than shipping dead buttons.
|
||||
export function ImageAdmin() {
|
||||
const { t } = useTranslation("admin");
|
||||
const { data, error, loading, reload } = useAsync(() => api.listImages(), []);
|
||||
const images = data ?? [];
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Images</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{t("images_title")}</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The platform image whitelist. Only enabled images can back a new server.
|
||||
Editing the whitelist from the Web is auditable (spec §14) — the mutation
|
||||
routes are a pending admin-tier slice, so this view is read-only for now.
|
||||
{t("images_subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -31,8 +31,8 @@ export function ImageAdmin() {
|
||||
<ErrorState error={error} onRetry={reload} />
|
||||
) : images.length === 0 ? (
|
||||
<EmptyState
|
||||
title="No images whitelisted"
|
||||
hint="The whitelist is empty — a platform admin must add one (CLI for now)."
|
||||
title={t("no_images_title")}
|
||||
hint={t("no_images_hint")}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -50,11 +50,11 @@ export function ImageAdmin() {
|
||||
</div>
|
||||
{img.enabled ? (
|
||||
<span className="inline-flex shrink-0 items-center gap-1 text-xs text-primary">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" /> enabled
|
||||
<CheckCircle2 className="h-3.5 w-3.5" /> {t("enabled")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
|
||||
<CircleSlash className="h-3.5 w-3.5" /> disabled
|
||||
<CircleSlash className="h-3.5 w-3.5" /> {t("disabled")}
|
||||
</span>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ServerCard } from "@/components/ServerCard";
|
||||
import { CreateServerDialog } from "@/components/CreateServerDialog";
|
||||
@@ -17,16 +18,17 @@ import { useAsync, useConfig } from "@/lib/hooks";
|
||||
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 ?? [];
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Servers</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{t("servers")}</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create platform servers from the structured form and manage the ones
|
||||
you operate.
|
||||
{t("server_admin_subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
{cfg && <CreateServerDialog cfg={cfg} onCreated={reload} />}
|
||||
@@ -37,11 +39,11 @@ export function ServerAdmin() {
|
||||
) : error ? (
|
||||
<ErrorState error={error} onRetry={reload} />
|
||||
) : !cfg ? (
|
||||
<Loading label="Loading config…" />
|
||||
<Loading label={ts("common:loading_config")} />
|
||||
) : servers.length === 0 ? (
|
||||
<EmptyState
|
||||
title="No servers under your management yet"
|
||||
hint="Use Create server to provision one from the vetted spec."
|
||||
title={ts("no_servers_managed")}
|
||||
hint={ts("no_servers_managed_hint")}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
@@ -53,10 +55,7 @@ export function ServerAdmin() {
|
||||
|
||||
<Card className="border-dashed bg-transparent">
|
||||
<CardContent className="p-4 text-xs text-muted-foreground">
|
||||
Server creation goes through the structured form only — the platform maps
|
||||
your choices onto a vetted Kubernetes spec. Raw cluster config (host
|
||||
networking, host paths, arbitrary images, privileged pods) is never
|
||||
expressible here (spec §15).
|
||||
{ts("server_admin_footer")}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user