TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import { formatMs } from "@/lib/format";2import { EmptyState } from "@/components/ui/empty-state";3import { Timer } from "lucide-react";45const PHASES: Array<{ key: string; label: string; description: string }> = [6 { key: "dns_ms", label: "DNS", description: "Resolving the target hostname" },7 { key: "proxy_connect_ms", label: "Network connect", description: "Establishing the route through the selected network class" },8 { key: "tls_ms", label: "TLS", description: "TLS handshake with the origin" },9 { key: "origin_ms", label: "Origin", description: "Waiting for the origin server to respond and stream the body" },10 { key: "processing_ms", label: "Processing", description: "Format conversion, redaction and metering" },11];1213function read(timing: Record<string, number>, key: string): number | null {14 const v = timing[key] ?? timing[key.replace(/_ms$/, "")];15 return typeof v === "number" && Number.isFinite(v) ? v : null;16}1718/** Horizontal waterfall of request phases from the stored `timing` JSON. Server component. */19export function TimingWaterfall({ timing, totalMs }: { timing: Record<string, number> | null; totalMs: number | null }) {20 if (!timing || Object.keys(timing).length === 0) {21 return <EmptyState compact icon={Timer} title="No timing recorded" description="Timing breakdown is captured for completed requests when the project log level is metadata or higher." />;22 }23 const phases = PHASES.map((p) => ({ ...p, value: read(timing, p.key) })).filter((p) => p.value !== null) as Array<(typeof PHASES)[number] & { value: number }>;24 const total = read(timing, "total_ms") ?? totalMs ?? phases.reduce((a, p) => a + p.value, 0);25 const scale = Math.max(total, phases.reduce((a, p) => a + p.value, 0), 1);26 // Each phase starts where the previous one ended (cumulative offsets computed up front).27 const rows = phases.map((p, i) => ({ ...p, start: phases.slice(0, i).reduce((a, q) => a + q.value, 0) }));28 return (29 <div className="rounded-lg border border-border bg-bg-elevated">30 <ol className="divide-y divide-border">31 {rows.map((p) => {32 const left = (p.start / scale) * 100;33 const width = Math.max((p.value / scale) * 100, 0.5);34 return (35 <li key={p.key} className="grid grid-cols-[120px_1fr_72px] items-center gap-3 px-4 py-2.5 text-[13px] sm:grid-cols-[160px_1fr_88px]">36 <div className="min-w-0">37 <div className="font-medium">{p.label}</div>38 <div className="hidden truncate text-[11.5px] text-fg-subtle sm:block">{p.description}</div>39 </div>40 <div className="relative h-2.5 rounded-full bg-bg-muted" aria-hidden>41 <div className="absolute top-0 h-full rounded-full bg-accent" style={{ left: `${left}%`, width: `${width}%` }} />42 </div>43 <div className="text-right font-mono tabular">{formatMs(p.value)}</div>44 </li>45 );46 })}47 </ol>48 <div className="flex items-center justify-between border-t border-border bg-bg-subtle/60 px-4 py-2.5 text-[13px]">49 <span className="font-medium">Total</span>50 <span className="font-mono tabular font-semibold">{formatMs(total)}</span>51 </div>52 </div>53 );54}55