import { useCallback, useEffect, useMemo, useState } from "react" import { Link } from "react-router" import { Pause, Play, Plus, RefreshCw } from "lucide-react" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client" import { ActionsCell, BadgeCell, DataTable, DateCell, Pagination, useTable, type ActionItem, type BadgeTone, type Column, } from "@crema/table-ui" import { SearchInput } from "@crema/search-ui" import { AlertBanner, ConfirmDialog, EmptyState, LoadingOverlay } from "@crema/feedback-ui" import { AppShell } from "~/components/layout/app-shell" import { Button } from "~/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card" import { activateTenant, deactivateTenant, listTenants, suspendTenant, type Tenant, type TenantStatus, } from "~/lib/arcadia/tenants" import { pageTitle } from "~/lib/page-meta" import { useSession } from "~/lib/session" export const meta = () => pageTitle("Tenants") type PendingAction = { kind: "suspend" | "deactivate" tenant: Tenant } | null export default function TenantsRoute() { const session = useSession() const arcadia = useArcadiaClient() const [tenants, setTenants] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [pending, setPending] = useState(null) const [search, setSearch] = useState("") const refresh = useCallback(async () => { setError(null) setLoading(true) try { const list = await listTenants(arcadia) setTenants(list) } catch (err) { setError(err instanceof ArcadiaError ? err.message : "Failed to load tenants.") } finally { setLoading(false) } }, [arcadia]) useEffect(() => { if (session) refresh() }, [session, refresh]) const runAction = useCallback( async (action: PendingAction) => { if (!action) return try { if (action.kind === "suspend") await suspendTenant(arcadia, action.tenant.id) else await deactivateTenant(arcadia, action.tenant.id) setPending(null) await refresh() } catch (err) { setError(err instanceof ArcadiaError ? err.message : "Action failed.") setPending(null) } }, [arcadia, refresh], ) const columns = useMemo[]>( () => [ { id: "name", header: "Name", accessor: "name", sortable: true, cell: (t) => {t.name}, }, { id: "slug", header: "Slug", accessor: "slug", sortable: true, cell: (t) => ( {t.slug} ), }, { id: "status", header: "Status", accessor: "status", sortable: true, cell: (t) => , }, { id: "plan", header: "Plan", accessor: (t) => t.plan?.name ?? "", sortable: true, cell: (t) => {t.plan?.name ?? "—"}, }, { id: "created", header: "Created", accessor: "inserted_at", sortable: true, cell: (t) => , }, { id: "actions", header: "", align: "right", cell: (t) => ( ), }, ], [arcadia, refresh], ) const table = useTable({ data: tenants, columns, getRowId: (t) => t.id, initialPageSize: 25, initialSearch: search, }) // Keep useTable's search in lockstep with our SearchInput. useEffect(() => { table.setSearch(search) }, [search, table]) if (!session) { return (
Sign in required Tenant administration requires an admin session.
) } return (

Tenants

Multi-tenant workspaces on this arcadia deployment.

{error ? ( setError(null)}> {error} ) : null}
{table.total} of {tenants.length}
{table.total === 0 && !loading ? ( ) : ( <> t.id} sort={table.sort} onSortToggle={table.toggleSort} loading={loading && tenants.length > 0} stickyHeader /> )}
!o && setPending(null)} title="Suspend tenant?" description={ pending ? `${pending.tenant.name} will be suspended. Members won't be able to sign in until you reactivate.` : "" } confirmLabel="Suspend" variant="default" onConfirm={() => runAction(pending)} /> !o && setPending(null)} title="Deactivate tenant?" description={ pending ? `${pending.tenant.name} will be deactivated. This is more severe than suspending.` : "" } confirmLabel="Deactivate" variant="danger" onConfirm={() => runAction(pending)} />
) } function statusTone(status: TenantStatus): BadgeTone { if (status === "active") return "success" if (status === "suspended") return "warning" if (status === "deactivated") return "danger" return "default" } function rowActions( t: Tenant, arcadia: ReturnType, refresh: () => Promise, setPending: (p: PendingAction) => void, setError: (msg: string | null) => void, ): ActionItem[] { const items: ActionItem[] = [] if (t.status === "active") { items.push({ id: "suspend", label: "Suspend", icon: , onSelect: () => setPending({ kind: "suspend", tenant: t }), }) } else { items.push({ id: "activate", label: "Activate", icon: , onSelect: async () => { try { await activateTenant(arcadia, t.id) await refresh() } catch (err) { setError(err instanceof ArcadiaError ? err.message : "Activate failed.") } }, }) } items.push({ id: "deactivate", label: "Deactivate", destructive: true, onSelect: () => setPending({ kind: "deactivate", tenant: t }), }) return items }