SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
4.4 KB · 98 lines tsx
Raw Blame History
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