Fuma studio

Mock Request

Installation

npx shadcn@latest add http://fumastudio/com/r/mock-request.json
components/fumastudio/mock-request.tsx
"use client";

import { MockRequestBaseURL } from "@/components/fumastudio/base-url";
import { CodeBlock } from "@/components/fumastudio/code-block";
import { colors } from "@/components/fumastudio/colors";
import { CheckCircleFilled } from "@/components/fumastudio/icons";
import { AuthSchemes } from "@/components/fumastudio/scheme";
import { ServerDialog } from "@/components/fumastudio/server-dialog";
import { ResponseSnippets } from "@/components/fumastudio/snippet";
import {
	Accordion,
	AccordionContent,
	AccordionItem,
	AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
	Command,
	CommandEmpty,
	CommandGroup,
	CommandInput,
	CommandItem,
	CommandList,
} from "@/components/ui/command";
import {
	Dialog,
	DialogContent,
	DialogDescription,
	DialogHeader,
	DialogTitle,
	DialogTrigger,
} from "@/components/ui/dialog";
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuGroup,
	DropdownMenuItem,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
	Popover,
	PopoverContent,
	PopoverTrigger,
} from "@/components/ui/popover";
import {
	Select,
	SelectContent,
	SelectItem,
	SelectTrigger,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useClipboard } from "@/hooks/use-clipboard";
import { useDebounce } from "@/hooks/use-debounce";
import { useDynamicSegment } from "@/hooks/use-dynamic-segment";
import { httpCodes } from "@/lib/http-codes";
import { cn } from "@/lib/utils";
import { ApiKeyHeaderScheme, ApiKeyQueryScheme } from "@trythis/js-schema";
import { Snippet } from "@trythis/nextjs";
import { PageEntry, Server } from "@trythis/nextjs/utils";
import {
	AlertTriangle,
	AlertTriangleIcon,
	CheckIcon,
	ChevronDownIcon,
	CircleCheck,
	CircleX,
	Copy,
	Info,
	Loader,
	ShareIcon,
} from "lucide-react";
import parseJson from "parse-json";
import React, {
	ChangeEvent,
	createContext,
	ReactNode,
	useContext,
	useEffect,
	useState,
} from "react";

const NO_AUTH = {
	type: "null",
	description: "No auth configured",
} as const;

type MockRequestProps = {
	children: React.ReactNode;
	open: boolean;
	url: string;
	onOpenChange: (state: boolean) => void;
};

const MockRequest = (props: MockRequestProps) => {
	const [isOpen, setIsOpen] = useState(false);
	const {
		isLoading,
		responseStatus,
		requestBody,
		responseBody,
		responseHeaders,
		contentType,
		responseSnippets,
		onSend,
		setRequestBody,
	} = useMockRequest();

	return (
		<Dialog open={props.open} onOpenChange={props.onOpenChange}>
			<DialogTrigger asChild>{props.children}</DialogTrigger>
			<DialogContent
				className="p-0 max-w-4xl xl:max-w-4xl h-[99dvh] max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-3rem)] md:max-h-[calc(100dvh-6rem)]"
				hiddenCloseButton>
				<DialogHeader className="sr-only">
					<DialogTitle>Try it</DialogTitle>
					<DialogDescription>
						Test requests as fast as possible
					</DialogDescription>
				</DialogHeader>

				<div className="relative flex flex-col gap-y-2.5 p-2 w-[calc(100%-2rem)]x bg-background dark:bg-background-dark rounded-3xl border-standard">
					<div className="sticky top-0 z-50 bg-background-light dark:bg-background-dark flex gap-x-2 h-10">
						<RequestDropdown className="max-w-60 w-60" />

						<MockRequestBaseURL
							url={props.url}
							className="w-full scrollbar-hide"
						/>

						<ButtonGroup>
							<Button
								variant="outline"
								disabled={isLoading}
								onClick={() => onSend()}>
								{isLoading ? (
									<>
										<Loader className="w-4 animate-spin duration-300x" />{" "}
										Wait
									</>
								) : (
									"Send"
								)}
							</Button>
							<DropdownMenu>
								<DropdownMenuTrigger asChild>
									<Button
										variant="outline"
										className="pl-2!"
										disabled={
											isLoading
										}>
										<ChevronDownIcon />
									</Button>
								</DropdownMenuTrigger>
								<DropdownMenuContent
									align="end"
									className="[--radius:1rem]">
									<DropdownMenuGroup>
										<DropdownMenuItem
											onClick={() =>
												setIsOpen(
													!isOpen,
												)
											}>
											<AlertTriangleIcon />
											List Servers
										</DropdownMenuItem>
										<DropdownMenuItem>
											<ShareIcon />
											Save options
										</DropdownMenuItem>
									</DropdownMenuGroup>
								</DropdownMenuContent>
							</DropdownMenu>
						</ButtonGroup>
					</div>

					<ServerDialog
						open={isOpen}
						onOpenChange={setIsOpen}
					/>

					<div className="grid grid-cols-2 gap-x-1.5 h-[calc(100%-2.5rem)]">
						<Accordion
							className="w-full px-2"
							collapsible
							defaultValue="Authorization"
							type="single">
							<AccordionItem value="Authorization">
								<AccordionTrigger className="py-1.5 text-[15px] leading-6 hover:no-underline text-sm">
									Authorization
								</AccordionTrigger>
								<AccordionContent className="h-full py-2.5">
									<AuthSchemeDropdown />
								</AccordionContent>
							</AccordionItem>
							<AccordionItem value="Body">
								<AccordionTrigger className="py-1.5 text-[15px] leading-6 hover:no-underline text-sm">
									Body
								</AccordionTrigger>
								<AccordionContent className="h-full py-2.5">
									<TextEditor />
								</AccordionContent>
							</AccordionItem>
						</Accordion>

						<div className="flex flex-col overflow-y-scroll gap-y-2.5 scrollbar-hide">
							{responseBody &&
								responseStatus &&
								contentType && (
									<div className="flex flex-col bg-accent border-input border gap-0 px-1.5 pb-1.5 rounded-xl">
										<ResponseBody
											status={
												responseStatus
											}
											body={
												responseBody
											}
											headers={
												responseHeaders
											}
											contentType={
												contentType
											}
										/>
									</div>
								)}

							{responseSnippets &&
								responseSnippets.length > 0 && (
									<div className="flex flex-col bg-accent border-input border gap-0 px-1.5 pb-1.5 rounded-xl">
										<ResponseSnippets
											snippets={
												responseSnippets
											}
										/>
									</div>
								)}
						</div>
					</div>
				</div>
			</DialogContent>
		</Dialog>
	);
};

const RequestDropdown = (props: { className?: string }) => {
	const { requests, selectedRequest, selectRequest } = useMockRequest();
	const [open, setOpen] = useState<boolean>(false);
	const [value, setValue] = useState<string>(selectedRequest?.slug || "");

	const onSelect = (currentValue: string) => {
		const slug = currentValue === value ? "" : currentValue;

		selectRequest(slug);
		setValue(slug);
		setOpen(false);
	};

	return (
		<div className={cn("*:not-first:mt-2", props.className)}>
			<Popover onOpenChange={setOpen} open={open}>
				<PopoverTrigger asChild>
					<Button
						aria-expanded={open}
						className="w-full justify-between border-input bg-background px-3 font-normal outline-none outline-offset-0 hover:bg-background focus-visible:outline-[3px]"
						role="combobox"
						variant="outline">
						<span
							className={cn(
								"truncate",
								!value &&
									"text-muted-foreground",
							)}>
							{value
								? requests.find(
										(request) =>
											request.slug ===
											value,
									)?.name
								: "Select request"}
						</span>
						<ChevronDownIcon
							aria-hidden="true"
							className="shrink-0 text-muted-foreground/80"
							size={16}
						/>
					</Button>
				</PopoverTrigger>
				<PopoverContent
					align="start"
					className="w-full min-w-(--radix-popper-anchor-width) border-input p-0"
					noPortal>
					<Command>
						<CommandInput placeholder="Search requests..." />
						<CommandList>
							<CommandEmpty>
								No request found.
							</CommandEmpty>
							<CommandGroup>
								{requests.map((request) => (
									<CommandItem
										key={request.slug}
										onSelect={onSelect}
										value={request.slug}
										className="flex items-center gap-2">
										<span
											className={`shrink-0 w-14 text-center font-geist-mono rounded-sm font-semibold py-0.5 text-xs leading-5 ${colors(request.method, { tryit: true })}`}>
											{
												request.method
											}
										</span>

										<span className="truncate flex-1">
											{request.name}
										</span>

										{value ===
											request.slug && (
											<CheckIcon
												className="ml-auto shrink-0"
												size={
													16
												}
											/>
										)}
									</CommandItem>
								))}
							</CommandGroup>
						</CommandList>
					</Command>
				</PopoverContent>
			</Popover>
		</div>
	);
};

const AuthSchemeDropdown = () => {
	const {
		authSchemes,
		selectedScheme,
		setScheme,
		authValues,
		setAuthValues,
	} = useMockRequest();

	const apiKeyQuery = authSchemes.find(
		(item) => item.type === "apiKeyQuery",
	)!;
	const apiKeyHeader = authSchemes.find(
		(item) => item.type === "apiKeyHeader",
	)!;

	const onChange =
		(key: keyof AuthValues) => (e: ChangeEvent<HTMLInputElement>) => {
			setAuthValues(key, e.target.value);
		};

	const currentAuthScheme =
		authSchemes.find((scheme) => scheme.type === selectedScheme) ||
		NO_AUTH;

	return (
		<div className="h-100 flex-1">
			<Select
				value={selectedScheme}
				onValueChange={(v: SecuritySchemes) => setScheme(v)}>
				<SelectTrigger className="h-14 flex gap-0.5 focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none ring-0 bg-accent">
					<div className="[&_*[role=option]>span]:start-auto [&_*[role=option]>span]:end-2 [&_*[role=option]]:ps-2 [&_*[role=option]]:pe-8">
						<div className="flex flex-col w-full text-start">
							<span>{selectedScheme}</span>

							{currentAuthScheme?.description && (
								<span className="mt-1 block text-muted-foreground text-xs">
									{
										currentAuthScheme.description
									}
								</span>
							)}
						</div>
					</div>
				</SelectTrigger>
				<SelectContent
					side="top"
					align="start"
					sideOffset={-60}
					className="[&_*[role=option]>span]:start-auto [&_*[role=option]>span]:end-2 [&_*[role=option]]:ps-2 [&_*[role=option]]:pe-8">
					{authSchemes.map((scheme) => (
						<SelectItem
							value={scheme.type}
							key={scheme.type}>
							{scheme.type}

							{scheme.description && (
								<span className="mt-1 block text-muted-foreground text-xs">
									{scheme.description}
								</span>
							)}
						</SelectItem>
					))}
				</SelectContent>
			</Select>

			<div className="mt-3.5">
				{selectedScheme === "bearerAuth" && (
					<>
						<div className="flex text-xs mb-1.5 items-center font-mono">
							<Label className="font-medium text-xs">
								Authorization (header) *
							</Label>
							<span className="ml-auto text-muted-foreground">
								string
							</span>
						</div>

						<div className="border border-input bg-muted flex items-center h-9 rounded-sm px-1.5 text-muted-foreground text-center">
							<span>Bearer</span>
							<Input
								className="focus-visible:ring-0 ml-1 focus-visible:ring-offset-0 focus-visible:outline-none border-0 ring-0 p-0"
								value={authValues?.token}
								onChange={onChange(
									"token",
								)}></Input>
						</div>
					</>
				)}

				{selectedScheme === "basicAuth" && (
					<div className="grid grid-cols-2 gap-x-1.5">
						<div>
							<div className="flex text-xs mb-1.5 items-center font-mono">
								<Label className="font-medium text-xs">
									username *
								</Label>
								<span className="ml-auto text-muted-foreground">
									string
								</span>
							</div>
							<div className="border border-input bg-muted flex items-center h-9 rounded-sm px-1.5 text-muted-foreground text-center">
								<Input
									className="focus-visible:ring-0 ml-1 focus-visible:ring-offset-0 focus-visible:outline-none border-0 ring-0 p-0"
									placeholder="Enter value"
									value={
										authValues?.username
									}
									onChange={onChange(
										"username",
									)}></Input>
							</div>
						</div>

						<div>
							<div className="flex text-xs mb-1.5 items-center font-mono">
								<Label className="font-medium text-xs">
									password *
								</Label>
								<span className="ml-auto text-muted-foreground">
									string
								</span>
							</div>
							<div className="border border-input bg-muted flex items-center h-9 rounded-sm px-1.5 text-muted-foreground text-center">
								<Input
									className="focus-visible:ring-0 ml-1 focus-visible:ring-offset-0 focus-visible:outline-none border-0 ring-0 p-0"
									placeholder="Enter value"
									value={
										authValues?.password
									}
									onChange={onChange(
										"password",
									)}></Input>
							</div>
						</div>
					</div>
				)}

				{selectedScheme === "apiKeyQuery" && (
					<>
						<div className="flex text-xs mb-1.5 items-center font-mono">
							<Label className="font-medium text-xs">
								{apiKeyQuery.name} (
								{apiKeyQuery.in}) *
							</Label>
							<span className="ml-auto text-muted-foreground">
								string
							</span>
						</div>

						<div className="border border-input bg-muted flex items-center h-9 rounded-sm px-1.5 text-muted-foreground text-center">
							<Input
								className="focus-visible:ring-0 ml-1 focus-visible:ring-offset-0 focus-visible:outline-none border-0 ring-0 p-0"
								placeholder="Enter value"
								value={authValues?.apiKeyQuery}
								onChange={onChange(
									"apiKeyQuery",
								)}></Input>
						</div>
					</>
				)}

				{selectedScheme === "apiKeyHeader" && (
					<>
						<div className="flex text-xs mb-1.5 items-center font-mono">
							<Label className="font-medium text-xs">
								{apiKeyHeader.name} (
								{apiKeyHeader.in}) *
							</Label>
							<span className="ml-auto text-muted-foreground">
								string
							</span>
						</div>

						<div className="border border-input bg-muted flex items-center h-9 rounded-sm px-1.5 text-muted-foreground text-center">
							<Input
								className="focus-visible:ring-0 ml-1 focus-visible:ring-offset-0 focus-visible:outline-none border-0 ring-0 p-0"
								placeholder="Enter value"
								value={authValues?.apiKeyHeader}
								onChange={onChange(
									"apiKeyHeader",
								)}></Input>
						</div>
					</>
				)}
			</div>
		</div>
	);
};

interface TextEditorProps {
	readOnly?: boolean;
}

function TextEditor({ readOnly = false }: TextEditorProps) {
	const {
		isLoading,
		requestBody: value,
		setRequestBody: onChange,
	} = useMockRequest();
	const { error } = useError(value);

	return (
		<div className="relative">
			<div className="flex flex-col bg-mutedx bg-gray-800/5 border-gray-400/30 border rounded-md h-full">
				<div className="overflow-auto h-full">
					<textarea
						value={value}
						onChange={(e) => onChange(e.target.value)}
						readOnly={readOnly || isLoading}
						className={`w-full scrollbar-hide h-77.5 p-2 font-geist-mono resize-none focus:outline-none ${
							readOnly ? "cursor-default" : ""
						}`}
						spellCheck={false}
					/>
				</div>

				<div
					className={`font-geist-mono p-2 text-xs text-red-400 border-gray-400/30 h-28 max-h-28 overflow-scroll scrollbar-hide ${error ? "border-t" : ""}`}>
					{error && <pre>{error}</pre>}
				</div>
			</div>
		</div>
	);
}

type ResponseBodyProps = {
	body: string;
	status: number;
	contentType: string;
	headers: any;
};

const ResponseBody = (props: ResponseBodyProps) => {
	const { body, status } = props;
	const [view, setView] = useState<Type>("body");
	const { isCopied, copyValue } = useClipboard();
	const httpStatus = httpCodes.find((item) => item.status === status);

	const { icon: Icon, className } = getStatusIcon(httpStatus?.status);

	if (!body) {
		return (
			<div className="flex items-center justify-center text-sm h-9 rounded-xl text-muted-foreground">
				No defined response.
			</div>
		);
	}

	return (
		<div className="w-full">
			<Tabs defaultValue="status" className="gap-0">
				<TabsList className="text-foreground w-full rounded-none bg-transparent px-1.5 h-10 flex items-center justify-between">
					<div className="flex flex-wrap items-center gap-x-1.5">
						<TabsTrigger
							value={"status"}
							className="gap-x-1.5 px-1.5 py-1 text-xs hover:text-foreground relative after:bottom-0 after:-mb-1 after:h-0.5 data-[state=active]:bg-transparent data-[state=active]:shadow-none">
							<div className={className}>
								<Icon className="h-4 w-4" />
							</div>

							<span className="max-w-35 truncate">
								{httpStatus?.status} –{" "}
								{httpStatus?.statusText}
							</span>
						</TabsTrigger>
					</div>

					<div className="ml-auto flex items-center gap-x-2.5">
						<Select
							value={view}
							onValueChange={(v: Type) =>
								setView(v)
							}>
							<SelectTrigger className="focus-visible:ring-0 h-8 hover:bg-muted-foreground/15 hover:border text-sm font-geist focus-visible:ring-offset-0 focus-visible:outline-none ring-0 pl-1.5 pr-1 py-1.25 rounded-lg">
								<span>{view}</span>
							</SelectTrigger>
							<SelectContent className="">
								<SelectItem
									value="body"
									className="text-sm font-geist">
									Body
								</SelectItem>
								<SelectItem
									value="headers"
									className="text-sm font-geist">
									Headers
								</SelectItem>
							</SelectContent>
						</Select>

						<button
							className="flex items-center transition-colors text-muted-foreground hover:text-foreground"
							onClick={() => {
								if (view === "body") {
									copyValue(props.body);
								}

								if (view === "headers") {
									copyValue(props.headers);
								}
							}}
							title="Copy snippet">
							{!isCopied && (
								<Copy className="size-4" />
							)}

							{isCopied && (
								<CheckCircleFilled className="size-5 cursor-pointer" />
							)}
						</button>
					</div>
				</TabsList>

				<TabsContent
					value="status"
					className="relative w-full overflow-scroll border h-44 max-h-48 scrollbar-hide border-input rounded-xl">
					<div className="text-sm">
						{view === "body" && (
							<CodeBlock
								code={props.body}
								language="json"
							/>
						)}
						{view === "headers" && (
							<CodeBlock
								code={props.headers}
								language="json"
							/>
						)}
					</div>
				</TabsContent>
			</Tabs>
		</div>
	);
};

const useError = (value: string) => {
	const debouncedValue = useDebounce(value);
	const [error, setError] = useState<string>("");

	useEffect(() => {
		if (!debouncedValue) {
			setError("");
			return;
		}

		try {
			parseJson(debouncedValue);
			setError("");
		} catch (err: any) {
			setError(err?.message ?? "Invalid JSON");
		}
	}, [debouncedValue]);

	return { error };
};

type Type = "body" | "headers";
type StatusIcon = {
	icon: React.ElementType;
	className: string;
};

function getStatusIcon(status?: number): StatusIcon {
	const statusIcons: Record<number, StatusIcon> = {
		1: { icon: Info, className: "text-blue-600 dark:text-blue-500" },
		2: {
			icon: CircleCheck,
			className: "text-green-600 dark:text-green-500",
		},
		3: {
			icon: AlertTriangle,
			className: "text-yellow-600 dark:text-yellow-500",
		},
		4: { icon: CircleX, className: "text-red-600 dark:text-red-500" },
		5: { icon: CircleX, className: "text-red-600 dark:text-red-500" },
	};

	const statusGroup = status ? Math.floor(status / 100) : 4;
	return statusIcons[statusGroup] ?? statusIcons[4];
}

export type Schemes = typeof NO_AUTH | AuthSchemes;

export type SecuritySchemes = Schemes[][number]["type"];

export type AuthValues = {
	token: string;
	username: string;
	password: string;
	apiKeyQuery: string;
	apiKeyHeader: string;
};

interface MockRequestState {
	baseURL: string;
	isLoading: boolean;
	responseBody: string | null;
	contentType: string | null;
	responseStatus: number | null;
	requestBody: string;
	requests: PageEntry[];
	selectedRequest: Omit<PageEntry, "data"> | null;
	authValues: AuthValues;
	responseHeaders: string | null;
	selectedScheme: SecuritySchemes;
	responseSnippets?: Snippet[];
	servers: Server[];
	authSchemes: Schemes[];
	responseSlug: string;
	onSend: () => Promise<void>;
	setBaseURL: (url: string) => void;
	setAuthValues: (key: keyof AuthValues, value: string) => void;
	setScheme: (scheme: SecuritySchemes) => void;
	setRequestBody: (val: string) => void;
	selectRequest: (slug: string) => void;
	setResponseSlug: (slug: string) => void;
}

type MockRequestProviderProps = {
	children: ReactNode;
	proxyPath?: string;
	intialRequests?: PageEntry[];
	currentRequest?: Omit<PageEntry, "data">;
	responseSnippets?: Snippet[];
	defaultRequestBody?: Record<string, any>;
	authSchemes?: Schemes[];
	servers: Server[];
	baseURL: string;
};

const MockRequestContext = createContext<MockRequestState | null>(null);

const MockRequestProvider = (props: MockRequestProviderProps) => {
	const apiPath = props.proxyPath || "/api/proxy";
	const [_responseSlug, _setResponseSlug] = useState("");
	const [_isLoading, _setIsLoading] = useState(false);
	const [_baseUrl, _setBaseUrl] = useState("");
	const { navigate } = useDynamicSegment();
	const [requests, setRequests] = useState<PageEntry[]>(
		props?.intialRequests || [],
	);
	const [_servers, _setServers] = useState<Server[]>(props?.servers || []);
	const [_authSchemes, _setAuthSchemes] = useState<Schemes[]>(() => {
		const item = [
			{
				type: "null",
				description: "No auth configured",
			} as typeof NO_AUTH,
		];

		if (props.authSchemes?.length === 0) {
			return item;
		}

		return props.authSchemes || item;
	});
	const [_responseSnippets, _setResponseSnippets] = useState<Snippet[]>([]);
	const [_requestBody, _setRequestBody] = useState("");
	const [_selectedRequest, _setSelectedRequest] = useState<Omit<
		PageEntry,
		"data"
	> | null>(props?.currentRequest || null);

	const [selectedScheme, setSelectedScheme] = useState<SecuritySchemes>(
		_authSchemes[0]?.type || "null",
	);

	const [_authValues, _setAuthValues] = useState<AuthValues>({
		token: "",
		username: "",
		password: "",
		apiKeyQuery: "",
		apiKeyHeader: "",
	});

	const [_responseStatus, _setResponseStatus] = useState<number | null>(
		null,
	);
	const [_responseBody, _setResponseBody] = useState<string | null>(null);
	const [_responseHeaders, _setResponseHeaders] = useState<string | null>(
		null,
	);
	const [_contentType, _setContentType] = useState<string | null>(null);

	const onSend = async (): Promise<void> => {
		if (!_selectedRequest) {
			console.log("No request was selected.");
			return;
		}
		if (!_baseUrl) {
			console.log("Baseurl was not found.");
			return;
		}
		const schemes = _authSchemes;

		try {
			console.log("OnSend: sending");

			_setIsLoading(true);
			const targetUrl = "x-proxy-target-url";

			let headers: Record<string, string> = {
				// TODO Trim double forward slashes
				[targetUrl]: `${_baseUrl}${props.baseURL}`,
			};

			const apiKeyQueryScheme = schemes.find(
				(s: any) => s.type === "apiKeyQuery",
			) as ApiKeyQueryScheme;

			const apiKeyHeaderScheme = schemes.find(
				(s: any) => s.type === "apiKeyHeader",
			) as ApiKeyHeaderScheme;

			if (selectedScheme === "bearerAuth") {
				headers.Authorization = `Bearer ${_authValues.token}`;
			}

			if (selectedScheme === "basicAuth") {
				const encoded = Buffer.from(
					`${_authValues.username}:${_authValues.password}`,
					"base64",
				).toString("base64");

				headers.Authorization = `Basic ${encoded}`;
			}

			if (selectedScheme === "apiKeyHeader" && apiKeyHeaderScheme) {
				headers[apiKeyHeaderScheme.name] =
					_authValues.apiKeyHeader;
			}

			if (selectedScheme === "apiKeyQuery" && apiKeyQueryScheme) {
				const query = new URLSearchParams({
					[apiKeyQueryScheme.name]: _authValues.apiKeyQuery,
				});

				headers[targetUrl] += `?${query.toString()}`;
			}

			console.log("_selectedRequest: ", _selectedRequest);

			const req = await window.fetch(apiPath, {
				method: _selectedRequest.method,
				headers,
				body:
					_selectedRequest.method !== "GET" &&
					_selectedRequest.method !== "HEAD"
						? _requestBody
						: undefined,
			});

			const contentType =
				req.headers.get("content-type")?.toLowerCase() || null;

			const res = await req.text();

			_setContentType(contentType);
			_setResponseBody(res);
			_setResponseStatus(req.status);
			_setResponseHeaders(
				JSON.stringify(
					Object.fromEntries(req.headers.entries()),
					null,
					2,
				),
			);

			_setIsLoading(false);
		} catch (error) {
			console.log("OnSend error: ", error);
			_setIsLoading(false);
		} finally {
			_setIsLoading(false);
		}
	};

	const setAuthValues = (key: keyof AuthValues, value: string) => {
		// TODO debounce this action
		_setAuthValues((p: AuthValues) => ({
			...p,
			[key]: value,
		}));
	};

	const setScheme = (scheme: SecuritySchemes) => {
		setSelectedScheme(scheme);
	};

	const setRequestBody = (val: string) => {
		_setRequestBody(val);
	};

	const selectRequest = (slug: string) => {
		if (!requests || requests.length === 0) {
			console.warn(
				"Requests are empty, there is nothing to select.",
			);
			return;
		}

		const req = requests.find((item) => item.slug === slug)!;

		if (!req) {
			return;
		}

		navigate(req.slug);
	};

	const setBaseURL = (value: string) => {
		_setBaseUrl(value);
	};

	const setResponseSlug = (slug: string) => {
		_setResponseSlug(slug);
	};

	useEffect(() => {
		if (
			_responseSlug &&
			props.defaultRequestBody &&
			typeof props.defaultRequestBody === "object"
		) {
			_setRequestBody(
				JSON.stringify(
					props.defaultRequestBody?.[_responseSlug] || {},
					null,
					2,
				),
			);
		}
	}, [_responseSlug, props.defaultRequestBody]);

	useEffect(() => {
		if (props.responseSnippets) {
			_setResponseSnippets(props.responseSnippets);
		}
	}, [props.responseSnippets]);

	return (
		<MockRequestContext.Provider
			value={{
				baseURL: _baseUrl,
				isLoading: _isLoading,
				responseStatus: _responseStatus,
				requestBody: _requestBody,
				requests,
				selectedRequest: _selectedRequest,
				authValues: _authValues,
				selectedScheme: selectedScheme,
				responseBody: _responseBody,
				contentType: _contentType,
				responseHeaders: _responseHeaders,
				responseSnippets: _responseSnippets,
				servers: _servers,
				authSchemes: _authSchemes,
				responseSlug: _responseSlug,
				onSend,
				setAuthValues,
				setScheme,
				setRequestBody,
				selectRequest,
				setBaseURL,
				setResponseSlug,
			}}>
			{props.children}
		</MockRequestContext.Provider>
	);
};

const useMockRequest = () => {
	const ctx = useContext(MockRequestContext);

	if (!ctx)
		throw new Error(
			"useMockRequest must be used inside MockRequestProvider",
		);
	return ctx;
};

export { MockRequest, MockRequestProvider, useMockRequest };

Usage

components/fumastudio/colors.tsx
// Internal component

Props