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