'use client'; import { Key, Send, Sparkles, Terminal } from 'lucide-react'; import React, { useCallback, useMemo } from 'react'; import { Combobox, type ComboboxOption, Input, Textarea, } from '@djangocfg/ui-core/components'; import { cn } from '@djangocfg/ui-core/lib'; import PrettyCode from '../../../../code/PrettyCode'; import { usePlaygroundContext } from '../../context/PlaygroundContext'; import { findApiKeyById, isValidJson, parseRequestHeaders } from '../../utils'; import { resolveAbsolute } from '../../utils/url'; import { BodyFormEditor } from './BodyFormEditor'; import { EndpointResetButton } from './EndpointResetButton'; import { CollapsibleSection, EmptyState, ScrollArea, SectionLabel, relativePath, } from './ui'; // ─── Param fields ───────────────────────────────────────────────────────────── type Param = { name: string; type: string; required: boolean; description?: string }; function ParamFields({ label, params }: { label: string; params: Param[] }) { const { state, setParameters } = usePlaygroundContext(); function handleChange(name: string, value: string) { setParameters({ ...state.parameters, [name]: value }); } return (
{label}
{params.map((p) => { const value = state.parameters[p.name] ?? ''; const placeholder = p.description || p.name; return (
{p.name} {p.required && ( * )} {p.type}
) => handleChange(p.name, e.target.value) } placeholder={placeholder} className="h-8 text-xs font-mono" />
); })}
); } // ─── RequestPanel ───────────────────────────────────────────────────────────── export function RequestPanel() { const { state, apiKeys, apiKeysLoading, setRequestBody, setRequestHeaders, setSelectedApiKey, setManualApiToken, sendRequest, } = usePlaygroundContext(); // Pre-compute every per-key view-model so the JSX stays a flat // render — no boolean math in the markup. const apiKeyOptions: ComboboxOption[] = useMemo( () => apiKeys.map((k) => ({ value: k.id, label: k.name || 'Unnamed key', // Surface the first 8 chars of the secret so the user // can tell two similarly-named keys apart at a glance. description: k.secret ? `${k.secret.slice(0, 8)}…` : undefined, })), [apiKeys], ); const hasApiKeys = apiKeyOptions.length > 0; const ep = state.selectedEndpoint; // ── Data (hooks must not be conditional) ───────────────────────────────── const isJsonValid = state.requestBody ? isValidJson(state.requestBody) : true; const curlCommand = useMemo(() => { if (!state.requestUrl) return ''; // Resolve to an absolute URL so the snippet is runnable from a // shell. The live ``fetch`` inside the playground resolves the // relative form itself via the browser; copy-pasting to curl // doesn't get that treatment. const absoluteUrl = resolveAbsolute(state.requestUrl); const apiKey = state.selectedApiKey ? findApiKeyById(apiKeys, state.selectedApiKey) : null; const hdrs = parseRequestHeaders(state.requestHeaders); if (apiKey) hdrs['X-API-Key'] = apiKey.secret || apiKey.id; let cmd = `curl -X ${state.requestMethod} "${absoluteUrl}"`; Object.entries(hdrs).forEach(([k, v]) => { cmd += ` \\\n -H "${k}: ${v}"`; }); if (state.requestBody && state.requestMethod !== 'GET' && isJsonValid) { cmd += ` \\\n -d '${state.requestBody}'`; } return cmd; }, [state, apiKeys, isJsonValid]); const pathParams = useMemo( () => ep?.parameters?.filter((p) => ep.path.includes(`{${p.name}}`)) ?? [], [ep], ); const queryParams = useMemo( () => ep?.parameters?.filter((p) => !ep.path.includes(`{${p.name}}`)) ?? [], [ep], ); // ── Derived ─────────────────────────────────────────────────────────────── const isSendDisabled = state.loading || !state.requestUrl || !isJsonValid; // Show the absolute URL in the meta row so the user sees exactly // what will go over the wire — same rewrite we do for cURL. const displayUrl = resolveAbsolute(state.requestUrl || ep?.path || ''); const hasBody = ep?.method !== 'GET'; const bodyType = ep?.requestBody?.type ?? ''; const hasPathParams = pathParams.length > 0; const hasQueryParams = queryParams.length > 0; const hasCurl = Boolean(curlCommand); const epPath = ep ? relativePath(ep.path) : ''; // Show the URL meta row only when it differs from the endpoint's // template shape — i.e. the user has substituted path params or the // URL host is worth showing explicitly. const urlChanged = displayUrl !== '' && displayUrl !== epPath; // ── Early return ────────────────────────────────────────────────────────── if (!ep) { return ; } // ── Render ──────────────────────────────────────────────────────────────── return ( <> {/* Inline meta row — shows the effective URL when it differs from the endpoint's path template (e.g. after substituting path parameters), plus the Reset action. We dropped the full endpoint header because the containing surface (SidePanel in docs layout, column header in classic layout) already shows the method + path, and duplicating the Send button next to a full-width Send at the bottom read as confusing. */} {(urlChanged || ep) && (
{urlChanged ? ( {displayUrl} ) : ( )}
)} {/* Scrollable fields */} {hasPathParams && } {hasQueryParams && } {/* Body */} {hasBody && ( )} {/* Auth & Headers — collapsed by default */} Auth & Headers } >
API Key setSelectedApiKey(v || null)} placeholder={ apiKeysLoading ? 'Loading keys…' : hasApiKeys ? 'Select an API key' : 'No API keys yet' } searchPlaceholder="Search keys…" emptyText="No matching key" disabled={apiKeysLoading || !hasApiKeys} className="h-8" />

Picks are sent via the{' '} X-API-Key header.

Bearer Token setManualApiToken(e.target.value)} className="font-mono text-xs h-8" />
Headers