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%
3.2 KB · 55 lines tsx
Raw Blame History
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