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