import { formatSize } from "@yaakapp-internal/lib/formatSize"; import type { Flow } from "@yaakapp-internal/proxy-lib"; import { CONTROL_TOOLTIP_DELAY, DetailsBanner, formatMillis, Icon, type IconProps, KeyValueRow, KeyValueRows, Tooltip, } from "@yaakapp-internal/ui"; import { format } from "date-fns"; import { Fragment, type HTMLAttributes, type ReactElement, type ReactNode, useMemo } from "react"; import { utcTimestamp } from "../lib/flowViews"; import { flowPhases, type PhaseKind } from "../lib/phases"; import { folderOf } from "../lib/search"; import { AppIcon } from "./AppIcon"; /** * Where a flow came from and how it travelled: in a proxy you didn't send the request, so the * connection on each leg, the app and its process, timing, sizes and the flow's record are part * of the answer. Then the timeline: each phase of the exchange and how long it took. */ export function FlowInfo({ flow }: { flow: Flow }) { const folder = folderOf(flow); const clientPort = portOf(flow.clientAddr); return (
{[ row("remote", "Remote Address", flow.remoteAddr, { copy: true }), row("client_version", "Client Version", flow.clientHttpVersion), row("server_version", "Server Version", flow.upstreamHttpVersion), row("tls", "TLS", flow.upstreamTlsVersion), row("cipher", "Cipher", flow.upstreamTlsCipher), row("client_port", "Client Port", clientPort), ]}
{[ row( "app", "Name", flow.clientName == null ? null : ( {flow.clientPath != null && } {flow.clientName} {folder != null && {folder}} ), ), flow.clientBundleId != null ? row("bundle", "Bundle ID", flow.clientBundleId, { copy: true }) : row("path", "Path", flow.clientPath, { copy: true }), row("pid", "PID", flow.clientPid == null ? null : `${flow.clientPid}`), row("cwd", "Directory", flow.clientCwd, { copy: true }), ]}
{[ row( "started", "Started", format(utcTimestamp(flow.startedAt), "MMM d, yyyy, h:mm:ss.SSS a"), ), row("headers", "Latency", ms(flow.elapsedHeadersMs)), row("total", "Time", ms(flow.elapsedMs)), ]}
{[ row("request_size", "Request", formatSize(flow.requestSize)), row("response_size", "Response", formatSize(flow.responseSize)), ]}
{[ row("number", "Number", `#${flow.number}`, { copy: true }), row("id", "ID", flow.id, { copy: true }), row("saved", "Yaak Request ID", flow.savedRequestId, { copy: true }), ]}
); } const PHASE_ICONS: Record = { start: { icon: "send_horizontal", color: "secondary" }, step: { icon: "arrow_right", color: "secondary" }, end: { icon: "check", color: "success" }, error: { icon: "alert_triangle", color: "danger" }, cancelled: { icon: "circle_off", color: "secondary" }, }; /** The flow's phases, each with how long it took and when, from the start */ function Timeline({ flow }: { flow: Flow }) { const phases = useMemo(() => flowPhases(flow), [flow]); if (phases.length === 0) return null; return ( Timeline}>
{phases.map((p) => { const { icon, color } = PHASE_ICONS[p.kind]; return ( {p.detail == null ? ( {p.label} ) : ( {p.label} {` ${p.detail}`} )} {p.kind === "start" ? "" : `+${formatMillis(p.tookMs)}`} {formatMillis(p.atMs)} ); })}
); } /** A row as `KeyValueRows` takes it */ type Row = ReactElement>; function Section({ title, children }: { title: string; children: (Row | null)[] }) { const rows = children.filter((r): r is Row => r != null); if (rows.length === 0) return null; return ( {title}}> {rows} ); } /** A row, or nothing when there's no value for it */ function row( key: string, label: string, value: ReactNode | null | undefined, opts: { copy?: boolean } = {}, ): Row | null { if (value == null || value === "") return null; return ( {value} ); } function ms(value: number | null): string | null { return value == null ? null : formatMillis(value); } /** The port of `127.0.0.1:54321` or `[::1]:54321` */ export function portOf(addr: string | null | undefined): string | null { const port = addr?.match(/:(\d+)$/)?.[1]; return port ?? null; } function copyText(text: string) { navigator.clipboard.writeText(text).catch(console.error); }