Files
pieced-portal/src/components/tenants/TenantDetailClient.tsx
2026-04-09 22:16:22 +02:00

197 lines
5.4 KiB
TypeScript

"use client";
import { useTranslations } from "next-intl";
import { useEffect, useState, useCallback } from "react";
import { useParams } from "next/navigation";
import { PACKAGE_CATALOG, type PackageDef } from "@/lib/packages";
import { PhaseBadge } from "@/components/dashboard/InstanceStatus";
import PackageCard from "@/components/packages/PackageCard";
import WorkspaceEditor from "@/components/packages/WorkspaceEditor";
interface TenantCR {
metadata: {
name: string;
creationTimestamp: string;
resourceVersion: string;
};
spec: {
agentName?: string;
displayName?: string;
packages?: string[];
workspaceFiles?: { name: string; content: string }[];
litellmTeamId?: string;
};
status?: {
phase: string;
conditions?: {
type: string;
status: string;
message?: string;
reason?: string;
}[];
};
}
export default function TenantDetailClient() {
const t = useTranslations("tenantDetail");
const { name } = useParams<{ name: string }>();
const [tenant, setTenant] = useState<TenantCR | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchTenant = useCallback(async () => {
try {
const res = await fetch(`/api/tenants/${name}`);
if (!res.ok) throw new Error(`${res.status}`);
setTenant(await res.json());
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
}, [name]);
useEffect(() => {
fetchTenant();
}, [fetchTenant]);
async function handlePackageToggle(
packageId: string,
enable: boolean
) {
if (!tenant) return;
const currentPackages = tenant.spec.packages || [];
const newPackages = enable
? [...currentPackages, packageId]
: currentPackages.filter((p) => p !== packageId);
const res = await fetch(`/api/tenants/${name}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ packages: newPackages }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Failed to update packages");
}
// Refetch tenant state
await fetchTenant();
}
async function handleWorkspaceSave(
files: { name: string; content: string }[]
) {
const res = await fetch(`/api/tenants/${name}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ workspaceFiles: files }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Failed to update workspace files");
}
await fetchTenant();
}
function getPackageStatus(
pkgId: string
): "pending" | "active" | "error" | undefined {
if (!tenant?.status?.conditions) return undefined;
const cond = tenant.status.conditions.find(
(c) => c.type === `Package/${pkgId}`
);
if (!cond) return "pending";
if (cond.status === "True") return "active";
if (cond.status === "False") return "error";
return "pending";
}
if (loading) {
return (
<div className="space-y-4 animate-pulse">
<div className="h-8 w-48 bg-zinc-800 rounded" />
<div className="h-40 bg-zinc-900/50 border border-zinc-800 rounded-lg" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{[1, 2, 3].map((i) => (
<div key={i} className="h-28 bg-zinc-900/50 border border-zinc-800 rounded-lg" />
))}
</div>
</div>
);
}
if (error || !tenant) {
return (
<div className="rounded-lg border border-red-900/30 bg-red-950/20 p-4 text-sm text-red-400">
{error || t("notFound")}
</div>
);
}
const enabledPackages = tenant.spec.packages || [];
const workspaceFiles = tenant.spec.workspaceFiles || [
{ name: "SOUL.md", content: "" },
{ name: "AGENTS.md", content: "" },
{ name: "TOOLS.md", content: "" },
];
return (
<div className="space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-zinc-100">
{tenant.spec.displayName || name}
</h1>
{tenant.spec.agentName && (
<p className="text-sm text-zinc-500 mt-0.5">
{t("agent")}: {tenant.spec.agentName}
</p>
)}
</div>
<PhaseBadge phase={tenant.status?.phase || "Pending"} />
</div>
{/* Packages */}
<section>
<h2 className="text-sm font-medium text-zinc-300 mb-3">
{t("packages")}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{PACKAGE_CATALOG.map((pkg) => (
<PackageCard
key={pkg.id}
pkg={pkg}
enabled={enabledPackages.includes(pkg.id)}
status={
enabledPackages.includes(pkg.id)
? getPackageStatus(pkg.id)
: undefined
}
tenantName={name}
onToggle={handlePackageToggle}
/>
))}
</div>
</section>
{/* Workspace files */}
<section>
<h2 className="text-sm font-medium text-zinc-300 mb-3">
{t("workspaceFiles")}
</h2>
<WorkspaceEditor
tenantName={name}
files={workspaceFiles}
onSave={handleWorkspaceSave}
/>
</section>
</div>
);
}