TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { FolderKanban } from "lucide-react";3import { requireAdmin } from "@/lib/session";4import { listProjects, pageNum, str } from "@/lib/queries/admin";5import { formatNumber, formatUsd } from "@/lib/format";6import { PageHeader } from "@/components/ui/page-header";7import { Badge } from "@/components/ui/badge";8import { EmptyState } from "@/components/ui/empty-state";9import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";10import { DateCell, Mono, Pagination, Panel, PlanBadge, SearchForm, numCell } from "@/components/admin/primitives";1112export const dynamic = "force-dynamic";1314export default async function AdminProjectsPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {15 await requireAdmin();16 const sp = await searchParams;17 const q = str(sp.q);18 const archived = str(sp.archived) === "1";19 const page = pageNum(sp.page);20 const data = await listProjects({ q: q || undefined, page, includeArchived: archived });2122 return (23 <>24 <PageHeader25 eyebrow="Customers"26 title="Projects"27 description={`${formatNumber(data.total)} projects across all organizations. Requests and spend are for the last 30 days.`}28 actions={29 <SearchForm action="/admin/projects" q={q} placeholder="project or organization name">30 <label className="flex items-center gap-1.5 text-[13px] text-fg-muted">31 <input type="checkbox" name="archived" value="1" defaultChecked={archived} className="size-3.5 accent-[var(--accent)]" /> include archived32 </label>33 </SearchForm>34 }35 />36 <Panel flush>37 {data.total === 0 ? (38 <div className="p-4">39 <EmptyState icon={FolderKanban} title="No projects match" compact />40 </div>41 ) : (42 <>43 <Table>44 <TableHeader>45 <TableRow>46 <TableHead>Project</TableHead>47 <TableHead>Organization</TableHead>48 <TableHead>Env</TableHead>49 <TableHead className="text-right">Requests 30d</TableHead>50 <TableHead className="text-right">Spend 30d</TableHead>51 <TableHead className="text-right">Keys</TableHead>52 <TableHead className="text-right">Created</TableHead>53 <TableHead>Status</TableHead>54 </TableRow>55 </TableHeader>56 <TableBody>57 {data.rows.map((p) => (58 <TableRow key={p.id}>59 <TableCell>60 <Link href={`/admin/requests?project=${p.id}`} className="font-medium hover:underline">61 {p.name}62 </Link>63 <div>64 <Mono className="text-[11.5px] text-fg-subtle">{p.id}</Mono>65 </div>66 </TableCell>67 <TableCell>68 <Link href={`/admin/organizations/${p.organizationId}`} className="hover:underline">69 {p.orgName}70 </Link>71 <span className="ml-2">72 <PlanBadge plan={p.orgPlan} />73 </span>74 </TableCell>75 <TableCell>76 <Badge variant={p.environment === "development" ? "warning" : "success"} dot>77 {p.environment}78 </Badge>79 </TableCell>80 <TableCell className={numCell}>{formatNumber(p.requests30d)}</TableCell>81 <TableCell className={numCell}>{formatUsd(p.spend30d)}</TableCell>82 <TableCell className={numCell}>{p.keys}</TableCell>83 <TableCell className="text-right">84 <DateCell value={p.createdAt} />85 </TableCell>86 <TableCell>{p.archivedAt ? <Badge variant="outline">archived</Badge> : <Badge variant="success" dot>active</Badge>}</TableCell>87 </TableRow>88 ))}89 </TableBody>90 </Table>91 <Pagination page={data.page} pages={data.pages} total={data.total} pageSize={data.pageSize} basePath="/admin/projects" params={{ q, archived: archived ? "1" : undefined }} />92 </>93 )}94 </Panel>95 </>96 );97}98