import { formatSize } from "@yaakapp-internal/lib/formatSize"; import type { Flow, FlowBody } from "@yaakapp-internal/proxy-lib"; import { CONTROL_TOOLTIP_DELAY, CountBadge, Dropdown, EmptyStateText, type ExtraPaneTab, FormUrlencodedEditor, type HttpBodyResult, HttpRequestPaneView, HttpResponsePaneView, Icon, IconButton, parseCookieHeaders, ResponseCookies, SplitLayout, Tooltip, } from "@yaakapp-internal/ui"; import classNames from "classnames"; import { useAtomValue, useSetAtom, useStore } from "jotai"; import { useCallback, useMemo } from "react"; import { BODY_TYPE_FORM_URLENCODED } from "@yaakapp-internal/ui/httpTypes"; import { placeTabsAtom } from "../lib/atoms"; import { findHeader } from "../lib/body"; import { browserTabMemory, defaultTabFor, type FlowPane, learnTab, REQUEST_TAB_AUTH, REQUEST_TAB_BODY, REQUEST_TAB_COOKIES, REQUEST_TAB_HEADERS, REQUEST_TAB_PARAMS, RESPONSE_TAB_BODY, RESPONSE_TAB_COOKIES, RESPONSE_TAB_HEADERS, RESPONSE_TAB_INFO, RESPONSE_TAB_MESSAGES, RESPONSE_TAB_TIMELINE, } from "../lib/defaultTab"; import { saveDialogFlowAtom, useFlowActions } from "../lib/flowActions"; import { type FlowBodyData, flowBodyLoader, flowCookiesView, flowRequestToView, flowResponseToView, } from "../lib/flowViews"; import { messagesTabLabel } from "../lib/messages"; import { goToPlace } from "../lib/navigation"; import { FlowBodyViewer, useFlowBody } from "./FlowBodyViewer"; import { FlowInfo } from "./FlowInfo"; import { FlowRequestBody } from "./FlowRequestBody"; import { FlowMessages } from "./FlowMessages"; import { FlowTimeline } from "./FlowTimeline"; import { ProxyEditorHost } from "./ProxyEditorHost"; /** * One card for a flow: the request half (its URL line and tabs), a hairline, and the response * half (its status line and tabs). The halves sit perpendicular to the list: stacked when the * list is beside the card, side by side when it is above. */ export function FlowCard({ flow, listBeside }: { flow: Flow; listBeside: boolean }) { return ( (
)} secondSlot={({ style, orientation }) => (
)} />
); } /** * The tab a half opens on, and learning from the ones chosen by hand. The tab chosen is part of * the window's place, so going back to it, or reloading, opens it again. */ function useShapeTab(pane: FlowPane, flow: Flow, available: string[]) { const store = useStore(); const memory = browserTabMemory(); const activeTab = useAtomValue(placeTabsAtom)[pane]; // Read again for every flow, so a tab learned on one opens the next of its shape const defaultTab = defaultTabFor(pane, flow, available, memory); const onTabChange = useCallback( (tab: string) => { learnTab(pane, flow, tab, memory); goToPlace(store, "update", { tabs: { ...store.get(placeTabsAtom), [pane]: tab } }); }, [pane, flow, memory, store], ); return { activeTab, defaultTab, onTabChange }; } /** A body read through the engine's body RPC, as the shared panes' loaders ask for it */ function useLoadedBody( flow: Flow, which: FlowBody, enabled = true, ): HttpBodyResult { const size = which === "request" ? flow.requestSize : flow.responseSize; const load = enabled && size > 0; const body = useFlowBody(flow, which, load); if (!load) return { data: null, isLoading: false, error: null }; return { data: body.data ?? undefined, isLoading: body.isLoading, error: body.error }; } function FlowRequestHalf({ flow }: { flow: Flow }) { const body = useLoadedBody(flow, "request"); const request = useMemo(() => flowRequestToView(flow, body.data ?? null), [flow, body.data]); const loader = useMemo( () => flowBodyLoader({ useBody: () => useLoadedBody(flow, "request"), contentType: findHeader(flow.requestHeaders, "content-type"), }), [flow], ); const cookies = useMemo(() => flowCookiesView(flow), [flow]); const sentCount = useMemo( () => parseCookieHeaders(cookies.cookieHeaders).sentCookies.length, [cookies], ); const extraTabs = useMemo( () => [ { tab: { value: REQUEST_TAB_COOKIES, label: "Cookies", rightSlot: , }, content: , }, ], [cookies, sentCount], ); const available = [ REQUEST_TAB_BODY, REQUEST_TAB_PARAMS, REQUEST_TAB_HEADERS, ...(request.auth == null ? [] : [REQUEST_TAB_AUTH]), REQUEST_TAB_COOKIES, ]; const { activeTab, defaultTab, onTabChange } = useShapeTab("request", flow, available); const forceUpdateKey = `${flow.updatedAt}.${body.data == null ? "" : "loaded"}`; return ( } renderBody={() => ( ( ) : undefined } hideCutNote /> } requestSize={flow.requestSize} /> )} /> ); } /** The method and URL, at the status line's height so the two header lines align */ function UrlLine({ flow }: { flow: Flow }) { return ( // The whole URL on hover, when it runs past the card
{flow.method} {flow.url}
); } function FlowResponseHalf({ flow, className }: { flow: Flow; className: string }) { // A stream's body is its messages, read by the Messages tab; reading it whole again on every // message would only hold copies of it const loadBody = flow.streamKind == null; const body = useLoadedBody(flow, "response", loadBody); const response = useMemo(() => flowResponseToView(flow, body.data ?? null), [flow, body.data]); const loader = useMemo( () => flowBodyLoader({ useBody: () => useLoadedBody(flow, "response", loadBody), contentType: findHeader(flow.responseHeaders, "content-type"), }), [flow, loadBody], ); const openSave = useSetAtom(saveDialogFlowAtom); const flowActions = useFlowActions(); const messagesLabel = messagesTabLabel(flow); const extraTabs = useMemo( () => [ { tab: { value: RESPONSE_TAB_TIMELINE, label: "Timeline" }, content: , }, { tab: { value: RESPONSE_TAB_INFO, label: "Info" }, content: }, ...(messagesLabel == null ? [] : [ { tab: { value: RESPONSE_TAB_MESSAGES, label: messagesLabel, rightSlot: ( ), }, content: , }, ]), ], [flow, messagesLabel], ); const available = [ RESPONSE_TAB_BODY, RESPONSE_TAB_HEADERS, RESPONSE_TAB_COOKIES, RESPONSE_TAB_TIMELINE, RESPONSE_TAB_INFO, ...(messagesLabel == null ? [] : [RESPONSE_TAB_MESSAGES]), ]; const { activeTab, defaultTab, onTabChange } = useShapeTab("response", flow, available); const saved = flow.savedRequestId != null; return ( Waiting for a response} tabsStorageKey="proxy_response_tabs" activeTabKey={flow.id} defaultTab={defaultTab} activeTab={activeTab} onTabChange={onTabChange} extraTabs={extraTabs} renderBodyViewer={() => } statusLineCenter={ flow.responseTruncated ? ( {body.data == null ? "Body cut" : `First ${formatSize(body.data.storedSize)} kept`} ) : undefined } statusLineRight={
openSave(flow)} />
} /> ); }