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%
10.7 KB · 224 lines tsx
Raw Blame History
1import Link from "next/link";2import { ListTree } from "lucide-react";3import { requireAdmin } from "@/lib/session";4import { CONCRETE_NETWORKS, PROVIDER_IDS, getRequestFilterOptions, listRequests, requestFiltersFromParams, str } from "@/lib/queries/admin";5import { formatBytes, formatMs, formatNumber, formatUsd } from "@/lib/format";6import { PageHeader } from "@/components/ui/page-header";7import { StatusBadge } from "@/components/ui/badge";8import { Button } from "@/components/ui/button";9import { Input, NativeSelect } from "@/components/ui/input";10import { Field, Label } from "@/components/ui/label";11import { EmptyState } from "@/components/ui/empty-state";12import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";13import { DateCell, Mono, NetworkBadge, Pagination, Panel, ProviderBadge, numCell } from "@/components/admin/primitives";1415export const dynamic = "force-dynamic";1617export default async function AdminRequestsPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {18  await requireAdmin();19  const sp = await searchParams;20  const f = requestFiltersFromParams(sp);21  const [data, opts] = await Promise.all([listRequests(f), getRequestFilterOptions()]);22  const params: Record<string, string | undefined> = {23    org: f.org,24    project: f.project,25    domain: f.domain,26    status: f.status,27    provider: f.provider,28    network: f.network,29    country: f.country,30    source: f.source,31    errorCode: f.errorCode,32    from: f.from,33    to: f.to,34    id: f.id,35  };36  const active = Object.values(params).filter(Boolean).length;3738  return (39    <>40      <PageHeader eyebrow="Explorer" title="Requests" description={`${formatNumber(data.total)} matching requests. Provider chain is shown per request (admin-only).`} />4142      <Panel className="mb-4" title="Filters" right={active ? <Button asChild variant="ghost" size="xs"><Link href="/admin/requests">Clear {active}</Link></Button> : null}>43        <form method="get" action="/admin/requests" className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6">44          <Field>45            <Label htmlFor="f-id">Request id</Label>46            <Input id="f-id" name="id" defaultValue={f.id} placeholder="req_…" className="h-8 font-mono text-[12.5px]" />47          </Field>48          <Field>49            <Label htmlFor="f-org">Organization</Label>50            <NativeSelect id="f-org" name="org" defaultValue={f.org ?? ""} className="h-8 text-[13px]">51              <option value="">Any</option>52              {opts.orgs.map((o) => (53                <option key={o.id} value={o.id}>54                  {o.name}55                </option>56              ))}57            </NativeSelect>58          </Field>59          <Field>60            <Label htmlFor="f-project">Project id</Label>61            <Input id="f-project" name="project" defaultValue={f.project} placeholder="proj_…" className="h-8 font-mono text-[12.5px]" />62          </Field>63          <Field>64            <Label htmlFor="f-domain">Domain</Label>65            <Input id="f-domain" name="domain" defaultValue={f.domain} placeholder="contains…" className="h-8 text-[13px]" />66          </Field>67          <Field>68            <Label htmlFor="f-status">Status</Label>69            <NativeSelect id="f-status" name="status" defaultValue={f.status ?? ""} className="h-8 text-[13px]">70              <option value="">Any</option>71              <option value="success">success</option>72              <option value="failed">failed</option>73              <option value="pending">pending</option>74            </NativeSelect>75          </Field>76          <Field>77            <Label htmlFor="f-provider">Provider</Label>78            <NativeSelect id="f-provider" name="provider" defaultValue={f.provider ?? ""} className="h-8 font-mono text-[12.5px]">79              <option value="">Any</option>80              {PROVIDER_IDS.map((p) => (81                <option key={p} value={p}>82                  {p}83                </option>84              ))}85            </NativeSelect>86          </Field>87          <Field>88            <Label htmlFor="f-network">Network</Label>89            <NativeSelect id="f-network" name="network" defaultValue={f.network ?? ""} className="h-8 text-[13px]">90              <option value="">Any</option>91              {CONCRETE_NETWORKS.map((n) => (92                <option key={n} value={n}>93                  {n}94                </option>95              ))}96              <option value="direct">direct</option>97            </NativeSelect>98          </Field>99          <Field>100            <Label htmlFor="f-country">Country</Label>101            <Input id="f-country" name="country" defaultValue={f.country} placeholder="CA" maxLength={2} className="h-8 font-mono uppercase text-[12.5px]" />102          </Field>103          <Field>104            <Label htmlFor="f-source">Source</Label>105            <NativeSelect id="f-source" name="source" defaultValue={f.source ?? ""} className="h-8 text-[13px]">106              <option value="">Any</option>107              <option value="api">api</option>108              <option value="playground">playground</option>109              <option value="sdk">sdk</option>110            </NativeSelect>111          </Field>112          <Field>113            <Label htmlFor="f-error">Error code</Label>114            <NativeSelect id="f-error" name="errorCode" defaultValue={f.errorCode ?? ""} className="h-8 font-mono text-[12.5px]">115              <option value="">Any</option>116              {opts.errorCodes.map((c) => (117                <option key={c} value={c}>118                  {c}119                </option>120              ))}121            </NativeSelect>122          </Field>123          <Field>124            <Label htmlFor="f-from">From</Label>125            <Input id="f-from" name="from" type="datetime-local" defaultValue={f.from} className="h-8 text-[12.5px]" />126          </Field>127          <Field>128            <Label htmlFor="f-to">To</Label>129            <Input id="f-to" name="to" type="datetime-local" defaultValue={f.to} className="h-8 text-[12.5px]" />130          </Field>131          <div className="flex items-end gap-2 sm:col-span-2 lg:col-span-4 xl:col-span-6">132            <Button type="submit" size="sm">133              Apply filters134            </Button>135            <span className="text-[12px] text-fg-subtle">Times are interpreted in the server timezone.</span>136          </div>137        </form>138      </Panel>139140      <Panel flush>141        {data.total === 0 ? (142          <div className="p-4">143            <EmptyState icon={ListTree} title="No requests match" description="Loosen a filter or widen the date range." compact />144          </div>145        ) : (146          <>147            <Table>148              <TableHeader>149                <TableRow>150                  <TableHead>Request</TableHead>151                  <TableHead>Org / project</TableHead>152                  <TableHead>Domain</TableHead>153                  <TableHead>Status</TableHead>154                  <TableHead>Network</TableHead>155                  <TableHead>Providers</TableHead>156                  <TableHead>Geo</TableHead>157                  <TableHead className="text-right">Att.</TableHead>158                  <TableHead className="text-right">Latency</TableHead>159                  <TableHead className="text-right">Bytes</TableHead>160                  <TableHead className="text-right">Cost</TableHead>161                  <TableHead className="text-right">Price</TableHead>162                  <TableHead className="text-right">When</TableHead>163                </TableRow>164              </TableHeader>165              <TableBody>166                {data.rows.map((r) => (167                  <TableRow key={r.id}>168                    <TableCell>169                      <Link href={`/admin/requests/${r.id}`} className="hover:underline">170                        <Mono>{r.id}</Mono>171                      </Link>172                      <div className="text-[11.5px] text-fg-subtle">{r.source}</div>173                    </TableCell>174                    <TableCell className="max-w-[180px]">175                      <Link href={`/admin/organizations/${r.organizationId}`} className="block truncate hover:underline">176                        {r.orgName ?? r.organizationId}177                      </Link>178                      <div className="truncate text-[11.5px] text-fg-subtle">{r.projectName ?? r.projectId}</div>179                    </TableCell>180                    <TableCell className="max-w-[220px] truncate" title={r.domain}>181                      <Link href={`/admin/domains/${encodeURIComponent(r.domain)}`} className="hover:underline">182                        {r.domain}183                      </Link>184                    </TableCell>185                    <TableCell>186                      <div className="flex items-center gap-1.5">187                        <StatusBadge status={r.status} />188                        {r.httpStatus ? <Mono className="text-fg-muted">{r.httpStatus}</Mono> : null}189                      </div>190                      {r.errorCode ? <div className="font-mono text-[11px] text-danger">{r.errorCode}</div> : null}191                    </TableCell>192                    <TableCell>193                      <NetworkBadge network={r.network} />194                      {r.requestedNetwork !== r.network ? <div className="text-[11px] text-fg-subtle">asked {r.requestedNetwork}</div> : null}195                    </TableCell>196                    <TableCell>197                      <div className="flex flex-wrap gap-1">198                        {r.providers ? r.providers.split(",").map((p, i) => <ProviderBadge key={i} id={p} />) : <span className="text-fg-subtle">—</span>}199                      </div>200                    </TableCell>201                    <TableCell>202                      <Mono>{r.country ?? "—"}</Mono>203                    </TableCell>204                    <TableCell className={numCell}>{r.attempts}</TableCell>205                    <TableCell className={numCell}>{formatMs(r.latencyMs)}</TableCell>206                    <TableCell className={numCell}>{formatBytes(r.bytesIn)}</TableCell>207                    <TableCell className={numCell}>{formatUsd(r.costUsd, true)}</TableCell>208                    <TableCell className={numCell}>{formatUsd(r.priceUsd, true)}</TableCell>209                    <TableCell className="text-right">210                      <DateCell value={r.createdAt} />211                    </TableCell>212                  </TableRow>213                ))}214              </TableBody>215            </Table>216            <Pagination page={data.page} pages={data.pages} total={data.total} pageSize={data.pageSize} basePath="/admin/requests" params={params} />217          </>218        )}219      </Panel>220      {str(sp.page) && data.rows.length === 0 && data.total > 0 ? <p className="mt-2 text-[12px] text-fg-subtle">This page is empty — go back to page 1.</p> : null}221    </>222  );223}224