"use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { Card } from "@/components/ui/card"; import type { OpenClawDefaults } from "@/lib/k8s"; interface TenantRow { name: string; displayName: string; phase: string; override: { tag: string } | null; } interface Props { initialDefaults: OpenClawDefaults; tenants: TenantRow[]; } /** * Two-section admin UI: * - Default editor card at the top — single input for the tag. * - Tenant table below — each row has an inline edit/clear control. * * No optimistic updates: every save round-trips to the API and we * router.refresh() to re-render the server-side state. Keeps the UI * honest about what's actually applied (controller-runtime watch * latency can be a couple of seconds). * * Tag-only by design — see operator notes for rationale. */ export function OpenClawAdminPanel({ initialDefaults, tenants }: Props) { const t = useTranslations("openclawAdmin"); const tCommon = useTranslations("common"); const router = useRouter(); const [defaults, setDefaults] = useState(initialDefaults); const [defaultTag, setDefaultTag] = useState(initialDefaults.defaultTag); const [savingDefault, setSavingDefault] = useState(false); const [defaultError, setDefaultError] = useState(""); const [defaultSaved, setDefaultSaved] = useState(false); const onSaveDefault = async (e: React.FormEvent) => { e.preventDefault(); setSavingDefault(true); setDefaultError(""); setDefaultSaved(false); try { const res = await fetch("/api/admin/openclaw", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ defaultTag: defaultTag.trim() }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || t("saveFailed")); } const next = await res.json(); setDefaults(next); setDefaultSaved(true); } catch (e: any) { setDefaultError(e.message); } finally { setSavingDefault(false); } }; return (
{/* Default editor */}

{t("defaultSection")}

{t("defaultDescription")}

setDefaultTag(e.target.value)} placeholder="2026.4.22" className="w-full px-3 py-2 rounded-lg border border-border bg-surface-2 text-text-primary text-sm font-mono focus:outline-none focus:border-text-secondary" />

{t("emptyHint")}

{defaultError && (
{defaultError}
)} {defaultSaved && !defaultError && (
{t("defaultSaved")}
)}
{/* Tenant overrides */}

{t("overridesSection")}

{tenants.length === 0 ? (

{t("noTenants")}

) : (
{tenants.map((tn) => ( router.refresh()} /> ))}
)}
); } /** * Single row in the tenants table. Collapsed by default; click to * expand the inline editor. */ function TenantOverrideRow({ tenant, platformDefault, onChanged, }: { tenant: TenantRow; platformDefault: OpenClawDefaults; onChanged: () => void; }) { const t = useTranslations("openclawAdmin"); const tCommon = useTranslations("common"); const [expanded, setExpanded] = useState(false); const [tag, setTag] = useState(tenant.override?.tag ?? ""); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const submit = async (clear = false) => { setSaving(true); setError(""); try { const res = await fetch( `/api/admin/tenants/${encodeURIComponent(tenant.name)}/openclaw-image`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(clear ? {} : { tag: tag.trim() }), } ); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || t("saveFailed")); } setExpanded(false); onChanged(); } catch (e: any) { setError(e.message); } finally { setSaving(false); } }; const effective = tenant.override?.tag ? tenant.override.tag : platformDefault.defaultTag || t("builtinFallback"); return (
{expanded && (
setTag(e.target.value)} placeholder={ platformDefault.defaultTag ? `${t("defaultPrefix")} ${platformDefault.defaultTag}` : "" } className="w-full px-3 py-2 rounded-lg border border-border bg-surface-2 text-text-primary text-sm font-mono focus:outline-none focus:border-text-secondary" />
{error && (
{error}
)}
{tenant.override && ( )}
)}
); }