import { formatSize } from "@yaakapp-internal/lib/formatSize"; import type { Flow } from "@yaakapp-internal/proxy-lib"; import { Banner, Button, CountBadge, Dropdown, type DropdownItem, formatMillis, HStack, HttpStatusTagRaw, Icon, SizeTag, type TabButtonRenderProps, type TabItem, TabContent, Tabs, } from "@yaakapp-internal/ui"; import classNames from "classnames"; import { atom, useAtomValue } from "jotai"; import type { CSSProperties } from "react"; import { useMemo, useState } from "react"; import { selectedFlowIdAtom } from "../lib/atoms"; import { findHeader, mimeEssence } from "../lib/body"; import { statusReason } from "../lib/statusReason"; import { flowsAtom } from "../lib/store"; import { EmptyState, FlowBodyViewer, useFlowBody } from "./FlowBodyViewer"; import { FlowHeaders, FlowQuery, queryParams } from "./FlowHeaders"; const selectedFlowAtom = atom((get) => { const id = get(selectedFlowIdAtom); return id == null ? null : (get(flowsAtom).find((f) => f.id === id) ?? null); }); const TAB_RESPONSE = "response"; const TAB_REQUEST = "request"; const TAB_HEADERS = "headers"; const TAB_QUERY = "query"; export function FlowDetail({ style, sideBySide }: { style?: CSSProperties; sideBySide: boolean }) { const flow = useAtomValue(selectedFlowAtom); return (
{flow == null ? (
Select a flow Start the proxy Stop the proxy
) : ( )}
); } function FlowPane({ flow }: { flow: Flow }) { const [pretty, setPretty] = useState(true); const responseBody = useFlowBody(flow, "response", flow.responseTruncated); // Failed before any response; an error after the headers is a body cut short mid-way. const noResponse = flow.error != null && flow.status == null; const mime = mimeEssence(findHeader(flow.responseHeaders, "content-type")); const queryCount = useMemo(() => queryParams(flow.url).length, [flow.url]); const tabs = useMemo( () => [ { value: TAB_RESPONSE, renderButton: ({ buttonProps, isActive }) => ( ), }, { value: TAB_REQUEST, label: "Request", rightSlot: flow.requestSize > 0 ? : null, }, { value: TAB_HEADERS, label: "Headers", rightSlot: ( ), }, { value: TAB_QUERY, label: "Query", rightSlot: , }, ], [ pretty, mime, flow.requestSize, flow.requestHeaders.length, flow.responseHeaders.length, queryCount, ], ); return (
{flow.elapsedMs != null && ( <> • {formatMillis(flow.elapsedMs)} )} {!noResponse && ( <> • )} {flow.responseTruncated ? ( {responseBody.data == null ? "Body cut" : `First ${formatSize(responseBody.data.storedSize)} kept`} ) : ( )}
{flow.error != null && ( {flow.error} )} {noResponse ? (
No response
The request never got a response. The Request tab has what the app sent.
) : ( )}
); } interface ViewModeButtonProps { buttonProps: TabButtonRenderProps["buttonProps"]; isActive: boolean; pretty: boolean; onChangePretty: (pretty: boolean) => void; prettyLabel: string; allowRaw: boolean; } /** The client's Response tab: a click on the active tab opens its view menu */ function ViewModeButton({ buttonProps, isActive, pretty, onChangePretty, prettyLabel, allowRaw, }: ViewModeButtonProps) { const showPretty = pretty || !allowRaw; const items: DropdownItem[] = [ { label: prettyLabel, leftSlot: , onSelect: () => onChangePretty(true), }, { label: `${prettyLabel} (Raw)`, hidden: !allowRaw, leftSlot: , onSelect: () => onChangePretty(false), }, ]; return ( ); } function CheckMark({ visible }: { visible: boolean }) { return ; } function Keys({ keys }: { keys: string[] }) { return ( {keys.map((k) => ( {k} ))} ); }