import { useCallback, useEffect, useMemo, useState } from "react" import { Link, useNavigate, useParams } from "react-router" import { ArrowLeft, Pause, Play, RefreshCw } from "lucide-react" import { useArcadiaClient } from "@crema/arcadia-core-client" import { BadgeCell, type BadgeTone } from "@crema/table-ui" import { ConfirmDialog } from "@crema/feedback-ui" import { useToast } from "@crema/notification-ui" import { useRegisterContext } from "@crema/aifirst-ui/context" import { AppShell } from "~/components/layout/app-shell" import { PageHeader } from "~/components/layout/page-header" import { DataState } from "~/components/data-state" import { TenantSection, Field } from "~/components/tenant-detail/section" import { Button } from "~/components/ui/button" import { Input } from "~/components/ui/input" import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs" import { activateTenant, deactivateTenant, getTenant, suspendTenant, updateTenant, type Tenant, type TenantStatus, } from "~/lib/arcadia/tenants" import { errorMessage } from "~/lib/errors" import { pageTitle } from "~/lib/page-meta" import { useSession } from "~/lib/session" // The eight tabs. Each is its own component so the file stays legible and the // tabs can evolve independently; they share the TenantSection frame and take // the loaded tenant + a `reload` callback. import { PlanTab } from "~/components/tenant-detail/plan-tab" import { BrandingTab } from "~/components/tenant-detail/branding-tab" import { LocalizationTab } from "~/components/tenant-detail/localization-tab" import { DeliveryTab } from "~/components/tenant-detail/delivery-tab" import { FeatureFlagsTab } from "~/components/tenant-detail/feature-flags-tab" import { IpRulesTab } from "~/components/tenant-detail/ip-rules-tab" import { InboundWebhooksTab } from "~/components/tenant-detail/inbound-webhooks-tab" export const meta = () => pageTitle("Tenant") export type TenantTabProps = { tenant: Tenant /** Re-fetch the tenant after a mutation that changes the header fields. */ reload: () => Promise } type PendingAction = { kind: "suspend" | "deactivate" } | null const TABS = [ { value: "overview", label: "Overview" }, { value: "plan", label: "Plan & quotas" }, { value: "branding", label: "Branding" }, { value: "localization", label: "Localization" }, { value: "delivery", label: "Email & SMS" }, { value: "flags", label: "Feature flags" }, { value: "ip-rules", label: "IP rules" }, { value: "webhooks", label: "Inbound webhooks" }, ] export default function TenantDetailRoute() { const { id = "" } = useParams() const session = useSession() const arcadia = useArcadiaClient() const toast = useToast() const navigate = useNavigate() const [tenant, setTenant] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [tab, setTab] = useState("overview") const [pending, setPending] = useState(null) const reload = useCallback(async () => { setError(null) setLoading(true) try { setTenant(await getTenant(arcadia, id)) } catch (err) { setError(err) } finally { setLoading(false) } }, [arcadia, id]) useEffect(() => { if (session && id) reload() }, [session, id, reload]) useRegisterContext( "tenantDetail", useMemo( () => ({ loaded: !!tenant, id: tenant?.id ?? id, slug: tenant?.slug ?? null, name: tenant?.name ?? null, status: tenant?.status ?? null, plan: tenant?.plan?.name ?? null, activeTab: tab, }), [tenant, tab, id], ), ) const runLifecycle = useCallback( async (action: PendingAction) => { if (!action || !tenant) return const verb = action.kind === "suspend" ? "Suspended" : "Deactivated" try { if (action.kind === "suspend") await suspendTenant(arcadia, tenant.id) else await deactivateTenant(arcadia, tenant.id) setPending(null) await reload() toast.success(`${verb} ${tenant.name}`) } catch (err) { setPending(null) toast.error(errorMessage(err, `${action.kind} ${tenant.name}`)) } }, [arcadia, tenant, reload, toast], ) const activate = useCallback(async () => { if (!tenant) return try { await activateTenant(arcadia, tenant.id) await reload() toast.success(`Activated ${tenant.name}`) } catch (err) { toast.error(errorMessage(err, `activate ${tenant.name}`)) } }, [arcadia, tenant, reload, toast]) return ( {tenant?.name ?? "Tenant"} } badges={ tenant ? ( <> {tenant.slug} ) : null } description={ All tenants } actions={ tenant ? ( <> {tenant.status === "active" ? ( ) : ( )} ) : null } /> Tenant not found.} > {tenant ? (
{TABS.map((t) => ( {t.label} ))}
) : null}
!o && setPending(null)} title="Suspend tenant?" description={ tenant ? `${tenant.name} will be suspended. Members won't be able to sign in until you reactivate.` : "" } confirmLabel="Suspend" variant="default" onConfirm={() => runLifecycle(pending)} /> !o && setPending(null)} title="Deactivate tenant?" description={ tenant ? `${tenant.name} will be taken offline: nobody can sign in and its apps stop serving. Its data is kept, and you can reactivate it from here. Suspend instead if this is temporary.` : "" } confirmLabel="Deactivate" variant="danger" onConfirm={() => runLifecycle(pending)} />
) } function statusTone(status: TenantStatus): BadgeTone { if (status === "active") return "success" if (status === "suspended") return "warning" if (status === "deactivated") return "danger" return "default" } // --- Overview tab (reference implementation for the other seven) --- function OverviewTab({ tenant, reload }: TenantTabProps) { const arcadia = useArcadiaClient() const toast = useToast() const [name, setName] = useState(tenant.name) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const dirty = name.trim() !== tenant.name && name.trim().length > 0 const save = async () => { setSaving(true) setError(null) try { await updateTenant(arcadia, tenant.id, { name: name.trim() }) await reload() toast.success("Tenant name updated") } catch (err) { setError(err) } finally { setSaving(false) } } return (
setName(e.target.value)} data-action="tenant-detail-overview-name" />
) } function FactCard({ label, value }: { label: string; value: string }) { return (
{label}
{value}
) } export { RouteErrorBoundary as ErrorBoundary } from "~/components/route-error"