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 · 58 lines tsx
Raw Blame History
1"use client";2import type { FetchTiming } from "@fetcha/core/client";3import { formatMs } from "@/lib/format";4import { cn } from "@/lib/utils";56const PHASES: Array<{ key: keyof FetchTiming; label: string; hint: string; color: string }> = [7  { key: "dns_ms", label: "DNS", hint: "Hostname resolution and SSRF validation", color: "bg-fg-subtle" },8  { key: "proxy_connect_ms", label: "Network connect", hint: "Connecting through the selected network class", color: "bg-info" },9  { key: "tls_ms", label: "TLS", hint: "TLS handshake with the target", color: "bg-accent" },10  { key: "origin_ms", label: "Origin", hint: "Time to receive the full response from the target", color: "bg-success" },11  { key: "processing_ms", label: "Processing", hint: "Decoding, format conversion and metadata", color: "bg-warning" },12];1314/** Horizontal breakdown of the request timing phases. */15export function TimingBars({ timing, totalFallbackMs }: { timing: FetchTiming | undefined; totalFallbackMs: number }) {16  if (!timing) {17    return <p className="text-sm text-fg-muted">No timing breakdown was returned for this request. Total duration: <span className="font-mono tabular">{formatMs(totalFallbackMs)}</span>.</p>;18  }19  const sum = PHASES.reduce((a, p) => a + Math.max(0, timing[p.key] ?? 0), 0);20  const total = Math.max(timing.total_ms || 0, sum, 1);21  return (22    <div className="grid gap-4">23      <div className="flex h-3 w-full overflow-hidden rounded-sm bg-bg-muted" role="img" aria-label={`Timing breakdown, total ${formatMs(total)}`}>24        {PHASES.map((p) => {25          const v = Math.max(0, timing[p.key] ?? 0);26          if (v <= 0) return null;27          return <div key={p.key} className={cn("h-full", p.color)} style={{ width: `${(v / total) * 100}%` }} title={`${p.label}: ${formatMs(v)}`} />;28        })}29      </div>30      <dl className="grid gap-2">31        {PHASES.map((p) => {32          const v = Math.max(0, timing[p.key] ?? 0);33          const pct = total ? (v / total) * 100 : 0;34          return (35            <div key={p.key} className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 sm:grid-cols-[9rem_minmax(0,1fr)_5.5rem]">36              <dt className="flex items-center gap-2 text-[13px]">37                <span className={cn("size-2 rounded-[2px]", p.color)} aria-hidden />38                <span className="font-medium">{p.label}</span>39                <span className="hidden text-fg-subtle sm:inline">· {p.hint}</span>40              </dt>41              <div className="hidden h-1.5 rounded-full bg-bg-muted sm:block" aria-hidden>42                <div className={cn("h-full rounded-full", p.color)} style={{ width: `${Math.min(100, pct)}%` }} />43              </div>44              <dd className="text-right font-mono text-[12.5px] tabular text-fg-muted">45                {formatMs(v)} <span className="text-fg-subtle">({pct.toFixed(0)}%)</span>46              </dd>47            </div>48          );49        })}50        <div className="mt-1 flex items-center justify-between border-t border-border pt-2 text-[13px]">51          <span className="font-medium">Total</span>52          <span className="font-mono tabular">{formatMs(timing.total_ms || total)}</span>53        </div>54      </dl>55    </div>56  );57}58