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);
}