import { useState, useEffect, useRef } from "react"; import { useAtomSet, useAtomValue, useAtomRefresh, Result } from "@effect-atom/atom-react"; import { Option } from "effect"; import { secretsAtom, setSecret, resolveSecret } from "@executor/react/api/atoms"; import { useScope } from "@executor/react/api/scope-context"; import { SecretPicker, type SecretPickerSecret } from "@executor/react/plugins/secret-picker"; import { SecretId } from "@executor/sdk"; import { Button } from "@executor/react/components/button"; import { Input } from "@executor/react/components/input"; import { Label } from "@executor/react/components/label"; import { Textarea } from "@executor/react/components/textarea"; import { Badge } from "@executor/react/components/badge"; import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group"; import { Spinner } from "@executor/react/components/spinner"; import { previewOpenApiSpec, addOpenApiSpec } from "./atoms"; import type { SpecPreview, HeaderPreset } from "../sdk/preview"; import type { HeaderValue } from "../sdk/types"; // --------------------------------------------------------------------------- // Inline secret creation // --------------------------------------------------------------------------- function InlineCreateSecret(props: { headerName: string; suggestedId: string; onCreated: (secretId: string) => void; onCancel: () => void; }) { const [secretId, setSecretId] = useState(props.suggestedId); const [secretName, setSecretName] = useState(props.headerName); const [secretValue, setSecretValue] = useState(""); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const scopeId = useScope(); const doSet = useAtomSet(setSecret, { mode: "promise" }); const refreshSecrets = useAtomRefresh(secretsAtom(scopeId)); const handleSave = async () => { if (!secretId.trim() || !secretValue.trim()) return; setSaving(true); setError(null); try { await doSet({ path: { scopeId }, payload: { id: SecretId.make(secretId.trim()), name: secretName.trim() || secretId.trim(), value: secretValue.trim(), purpose: `Auth header: ${props.headerName}`, }, }); refreshSecrets(); props.onCreated(secretId.trim()); } catch (e) { setError(e instanceof Error ? e.message : "Failed to save secret"); setSaving(false); } }; return (

New secret

setSecretId((e.target as HTMLInputElement).value)} placeholder="my-api-token" className="h-8 text-xs font-mono" />
setSecretName((e.target as HTMLInputElement).value)} placeholder="API Token" className="h-8 text-xs" />
setSecretValue((e.target as HTMLInputElement).value)} placeholder="paste your token or key…" className="h-8 text-xs font-mono" />
{error &&

{error}

}
); } // --------------------------------------------------------------------------- // Header value preview — shows what the header will look like // --------------------------------------------------------------------------- type ResolveState = | { status: "hidden" } | { status: "loading" } | { status: "revealed"; value: string } | { status: "error" }; function HeaderValuePreview(props: { headerName: string; secretId: string; prefix?: string; }) { const { headerName, secretId, prefix } = props; const scopeId = useScope(); const [state, setState] = useState({ status: "hidden" }); const doResolve = useAtomSet(resolveSecret, { mode: "promise" }); const handleToggle = async () => { if (state.status === "revealed") { setState({ status: "hidden" }); return; } setState({ status: "loading" }); try { const result = await doResolve({ path: { scopeId, secretId: SecretId.make(secretId), }, }); setState({ status: "revealed", value: result.value }); } catch { setState({ status: "error" }); } }; const displayValue = state.status === "revealed" ? state.value : state.status === "error" ? "failed to resolve" : "•".repeat(12); const isLoading = state.status === "loading"; const isRevealed = state.status === "revealed"; return (
{headerName}: {prefix && {prefix}} {displayValue}
); } // --------------------------------------------------------------------------- // Header presets // --------------------------------------------------------------------------- const HEADER_PRESETS = [ { key: "bearer", label: "Bearer Token", name: "Authorization", prefix: "Bearer " }, { key: "basic", label: "Basic Auth", name: "Authorization", prefix: "Basic " }, { key: "api-key", label: "API Key", name: "X-API-Key" }, { key: "auth-token", label: "Auth Token", name: "X-Auth-Token" }, { key: "access-token", label: "Access Token", name: "X-Access-Token" }, { key: "cookie", label: "Cookie", name: "Cookie" }, { key: "custom", label: "Custom", name: "" }, ] as const; // --------------------------------------------------------------------------- // Custom header row — pick a preset, then pick a secret // --------------------------------------------------------------------------- function CustomHeaderRow(props: { name: string; prefix?: string; presetKey?: string; secretId: string | null; onChange: (update: { name: string; prefix?: string; presetKey?: string }) => void; onSelectSecret: (secretId: string) => void; onRemove: () => void; existingSecrets: readonly SecretPickerSecret[]; }) { const [creating, setCreating] = useState(false); const { name, prefix, presetKey, secretId, onChange, onSelectSecret, onRemove, existingSecrets } = props; const isCustom = presetKey === "custom"; const suggestedId = name.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "custom-header"; if (creating) { return ( { onSelectSecret(id); setCreating(false); }} onCancel={() => setCreating(false)} /> ); } return (
{/* Preset chips */}
{HEADER_PRESETS.map((p) => ( ))}
{/* Name + prefix fields — always visible once a preset is picked */} {presetKey !== undefined && (
onChange({ name: (e.target as HTMLInputElement).value, prefix, presetKey: isCustom ? "custom" : presetKey })} placeholder="Authorization" className="h-8 text-xs font-mono" />
onChange({ name, prefix: (e.target as HTMLInputElement).value || undefined, presetKey: isCustom ? "custom" : presetKey })} placeholder="Bearer " className="h-8 text-xs font-mono" />
)} {/* Secret picker */} {presetKey !== undefined && name.trim() && (
)} {/* Preview */} {secretId && name.trim() && ( )}
); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function prefixForHeader(preset: HeaderPreset, headerName: string): string | undefined { const label = preset.label.toLowerCase(); if (headerName.toLowerCase() === "authorization") { if (label.includes("bearer")) return "Bearer "; if (label.includes("basic")) return "Basic "; } return undefined; } function matchPresetKey(name: string, prefix?: string): string { if (name === "Authorization" && prefix === "Bearer ") return "bearer"; if (name === "Authorization" && prefix === "Basic ") return "basic"; if (name === "X-API-Key") return "api-key"; if (name === "X-Auth-Token") return "auth-token"; if (name === "X-Access-Token") return "access-token"; if (name === "Cookie") return "cookie"; return "custom"; } function presetEntriesFromHeaderPreset(preset: HeaderPreset) { return preset.secretHeaders.map((headerName) => { const prefix = prefixForHeader(preset, headerName); return { name: headerName, secretId: null as string | null, prefix, presetKey: matchPresetKey(headerName, prefix), fromPreset: true, }; }); } // --------------------------------------------------------------------------- // Main component — single progressive form // --------------------------------------------------------------------------- export default function AddOpenApiSource(props: { onComplete: () => void; onCancel: () => void; initialUrl?: string; }) { // Spec input const [specUrl, setSpecUrl] = useState(props.initialUrl ?? ""); const [analyzing, setAnalyzing] = useState(false); const [analyzeError, setAnalyzeError] = useState(null); // After analysis const [preview, setPreview] = useState(null); const [baseUrl, setBaseUrl] = useState(""); // Auth const [presetIndex, setPresetIndex] = useState(0); const [customHeaders, setCustomHeaders] = useState>([]); // Submit const [adding, setAdding] = useState(false); const [addError, setAddError] = useState(null); const scopeId = useScope(); const doPreview = useAtomSet(previewOpenApiSpec, { mode: "promise" }); const doAdd = useAtomSet(addOpenApiSpec, { mode: "promise" }); const secrets = useAtomValue(secretsAtom(scopeId)); const autoAnalyzed = useRef(false); useEffect(() => { if (props.initialUrl && !autoAnalyzed.current) { autoAnalyzed.current = true; handleAnalyze(); } }, []); // eslint-disable-line react-hooks/exhaustive-deps const secretList: readonly SecretPickerSecret[] = Result.match(secrets, { onInitial: () => [] as SecretPickerSecret[], onFailure: () => [] as SecretPickerSecret[], onSuccess: ({ value }) => value.map((s) => ({ id: s.id, name: s.name, provider: s.provider ? String(s.provider) : undefined, })), }); // ---- Derived state ---- const presets = preview?.headerPresets ?? []; const hasAuth = presets.length > 0; const servers = (preview?.servers ?? []) as Array<{ url?: string }>; const allHeaders: Record = {}; for (const ch of customHeaders) { if (ch.name.trim() && ch.secretId) { allHeaders[ch.name.trim()] = { secretId: ch.secretId, ...(ch.prefix ? { prefix: ch.prefix } : {}) }; } } const hasHeaders = Object.keys(allHeaders).length > 0; const customHeadersValid = customHeaders.every( (ch) => ch.name.trim() && ch.secretId, ); const canAdd = preview !== null && baseUrl.trim().length > 0 && (customHeaders.length === 0 || customHeadersValid); // ---- Handlers ---- const handleAnalyze = async () => { setAnalyzing(true); setAnalyzeError(null); setAddError(null); try { const result = await doPreview({ path: { scopeId }, payload: { spec: specUrl }, }); setPreview(result); const firstUrl = (result.servers as Array<{ url?: string }>)?.[0]?.url; if (firstUrl) setBaseUrl(firstUrl); const newPresetIndex = result.headerPresets.length > 0 ? 0 : -1; setPresetIndex(newPresetIndex); setCustomHeaders( newPresetIndex >= 0 ? presetEntriesFromHeaderPreset(result.headerPresets[newPresetIndex]) : [], ); } catch (e) { setAnalyzeError(e instanceof Error ? e.message : "Failed to parse spec"); } finally { setAnalyzing(false); } }; const selectPreset = (index: number) => { setPresetIndex(index); if (index === -1) { // "None" — clear everything setCustomHeaders([]); } else if (index === -2) { // "Custom" — keep user headers, drop preset-derived, seed if empty const userHeaders = customHeaders.filter((h) => !h.fromPreset); setCustomHeaders(userHeaders.length > 0 ? userHeaders : [{ name: "", secretId: null, presetKey: undefined }]); } else { // Preset strategy — replace preset-derived headers, keep user headers const preset = presets[index]; const userHeaders = customHeaders.filter((h) => !h.fromPreset); setCustomHeaders(preset ? [...presetEntriesFromHeaderPreset(preset), ...userHeaders] : userHeaders); } }; const addCustomHeader = () => { if (presetIndex === -1) setPresetIndex(-2); setCustomHeaders([...customHeaders, { name: "", secretId: null, presetKey: undefined }]); }; const updateCustomHeader = (index: number, update: Partial<{ name: string; secretId: string | null; prefix?: string; presetKey?: string }>) => { setCustomHeaders(customHeaders.map((ch, i) => (i === index ? { ...ch, ...update } : ch))); }; const removeCustomHeader = (index: number) => { setCustomHeaders(customHeaders.filter((_, i) => i !== index)); }; const handleAdd = async () => { setAdding(true); setAddError(null); try { await doAdd({ path: { scopeId }, payload: { spec: specUrl, baseUrl: baseUrl.trim() || undefined, ...(hasHeaders ? { headers: allHeaders } : {}), }, }); props.onComplete(); } catch (e) { setAddError(e instanceof Error ? e.message : "Failed to add source"); setAdding(false); } }; // ---- Render ---- return (

Add OpenAPI Source

{/* ── Spec input ── */}