229 lines
8.5 KiB
TypeScript
229 lines
8.5 KiB
TypeScript
import { getSessionUser } from "@/lib/session";
|
|
import { getTranslations, getFormatter } from "next-intl/server";
|
|
import { redirect } from "next/navigation";
|
|
import { listTenants } from "@/lib/k8s";
|
|
import { getTenantRequestByOrgId } from "@/lib/db";
|
|
import { Card, CardHeader } from "@/components/ui/card";
|
|
import { StatusBadge } from "@/components/ui/status-badge";
|
|
import { UsageDisplay } from "@/components/dashboard/usage-display";
|
|
import { OnboardingFlow } from "@/components/onboarding/onboarding-flow";
|
|
import { formatDateTime } from "@/lib/format";
|
|
import Link from "next/link";
|
|
|
|
export default async function DashboardPage() {
|
|
const user = await getSessionUser();
|
|
if (!user) redirect("/login");
|
|
|
|
const t = await getTranslations("dashboard");
|
|
const tAdmin = await getTranslations("admin");
|
|
const f = await getFormatter();
|
|
|
|
const allTenants = await listTenants();
|
|
|
|
// Platform users see overview of all tenants
|
|
if (user.isPlatform) {
|
|
const phaseCount = allTenants.reduce<Record<string, number>>((acc, t) => {
|
|
const phase = t.status?.phase ?? "Pending";
|
|
acc[phase] = (acc[phase] || 0) + 1;
|
|
return acc;
|
|
}, {});
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-8 animate-in">
|
|
<h1 className="font-display text-2xl font-semibold accent-rule mb-2">
|
|
{t("title")}
|
|
</h1>
|
|
<p className="text-text-secondary text-sm mt-4">
|
|
{t("welcome", { name: user.name || user.email })}
|
|
</p>
|
|
</div>
|
|
|
|
<Link
|
|
href="/admin"
|
|
className="inline-flex items-center gap-1.5 mb-6 text-xs font-medium text-accent hover:text-accent-dim transition-colors animate-in animate-in-delay-1"
|
|
>
|
|
<span>→</span> {tAdmin("title")}
|
|
</Link>
|
|
|
|
{/* Summary cards */}
|
|
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4 mb-8 animate-in animate-in-delay-1">
|
|
<Card>
|
|
<CardHeader>{tAdmin("allTenants")}</CardHeader>
|
|
<span className="font-display text-3xl font-semibold text-text-primary tabular-nums">
|
|
{allTenants.length}
|
|
</span>
|
|
</Card>
|
|
{Object.entries(phaseCount).map(([phase, count]) => (
|
|
<Card key={phase}>
|
|
<CardHeader>{phase}</CardHeader>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-display text-3xl font-semibold text-text-primary tabular-nums">
|
|
{count}
|
|
</span>
|
|
<StatusBadge phase={phase} />
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tenant table */}
|
|
{allTenants.length > 0 && (
|
|
<div className="bg-surface-1 border border-border rounded-xl overflow-hidden animate-in animate-in-delay-2">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-border text-left">
|
|
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
|
{tAdmin("name")}
|
|
</th>
|
|
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
|
{tAdmin("phase")}
|
|
</th>
|
|
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
|
{tAdmin("packages")}
|
|
</th>
|
|
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
|
{tAdmin("created")}
|
|
</th>
|
|
<th className="px-5 py-3" />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{allTenants.map((tenant) => (
|
|
<tr
|
|
key={tenant.metadata.name}
|
|
className="border-b border-border last:border-0 hover:bg-surface-2/50 transition-colors"
|
|
>
|
|
<td className="px-5 py-3">
|
|
<div className="font-mono text-xs text-accent">
|
|
{tenant.metadata.name}
|
|
</div>
|
|
{tenant.spec.displayName && (
|
|
<div className="text-xs text-text-secondary">
|
|
{tenant.spec.displayName}
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className="px-5 py-3">
|
|
<StatusBadge phase={tenant.status?.phase ?? "Pending"} />
|
|
</td>
|
|
<td className="px-5 py-3 text-xs text-text-secondary font-mono">
|
|
{tenant.spec.packages?.join(", ") || "—"}
|
|
</td>
|
|
<td className="px-5 py-3 text-xs text-text-muted tabular-nums">
|
|
{formatDateTime(tenant.metadata.creationTimestamp, f)}
|
|
</td>
|
|
<td className="px-5 py-3 text-right">
|
|
<Link
|
|
href={`/tenants/${tenant.metadata.name}`}
|
|
className="text-xs font-medium text-accent hover:text-accent-dim transition-colors"
|
|
>
|
|
{t("manage")} →
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Regular user: find their tenant
|
|
const myTenant = allTenants.find(
|
|
(t) => t.metadata.labels?.["pieced.ch/zitadel-org-id"] === user.orgId
|
|
);
|
|
|
|
// No tenant → check for existing request, show onboarding flow
|
|
if (!myTenant) {
|
|
const existingRequest = await getTenantRequestByOrgId(user.orgId);
|
|
// Treat "deleted" as no request — customer can re-onboard
|
|
const initialState =
|
|
!existingRequest || existingRequest.status === "deleted"
|
|
? "no_request"
|
|
: existingRequest.status;
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-8 animate-in">
|
|
<h1 className="font-display text-2xl font-semibold accent-rule mb-2">
|
|
{t("title")}
|
|
</h1>
|
|
<p className="text-text-secondary text-sm mt-4">
|
|
{t("welcome", { name: user.name || user.email })}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="animate-in animate-in-delay-1">
|
|
<OnboardingFlow
|
|
orgName={user.orgName}
|
|
initialState={initialState as any}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const tenantName = myTenant.metadata.name;
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-8 animate-in">
|
|
<h1 className="font-display text-2xl font-semibold accent-rule mb-2">
|
|
{t("title")}
|
|
</h1>
|
|
<p className="text-text-secondary text-sm mt-4">
|
|
{t("welcome", { name: user.name || user.email })}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Instance status card */}
|
|
<div className="mb-6 animate-in animate-in-delay-1">
|
|
<Card>
|
|
<CardHeader>{t("instanceStatus")}</CardHeader>
|
|
<div className="flex items-center gap-4">
|
|
<StatusBadge phase={myTenant.status?.phase ?? "Pending"} />
|
|
{myTenant.spec.agentName && (
|
|
<span className="text-sm text-text-secondary">
|
|
{myTenant.spec.agentName}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{myTenant.spec.packages && myTenant.spec.packages.length > 0 && (
|
|
<div className="flex flex-wrap gap-2 mt-3">
|
|
{myTenant.spec.packages.map((pkg) => (
|
|
<span
|
|
key={pkg}
|
|
className="text-xs font-mono bg-accent/10 text-accent border border-accent/20 rounded-full px-2.5 py-0.5"
|
|
>
|
|
{pkg}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Usage — no teamId passed, backend resolves from session */}
|
|
<div className="mb-6 animate-in animate-in-delay-2">
|
|
<h2 className="text-xs font-semibold uppercase tracking-wider text-text-muted mb-3">
|
|
{t("usage")}
|
|
</h2>
|
|
<UsageDisplay />
|
|
</div>
|
|
|
|
{/* Link to tenant detail */}
|
|
<Link
|
|
href={`/tenants/${tenantName}`}
|
|
className="inline-flex items-center gap-1.5 text-xs font-medium text-accent hover:text-accent-dim transition-colors animate-in animate-in-delay-3"
|
|
>
|
|
<span>→</span> {t("manage")}
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|