import { useState } from "react"; import { useAtomSet } from "@effect-atom/atom-react"; import { useScope } from "@executor/react/api/scope-context"; import { SecretHeaderAuthRow } from "@executor/react/plugins/secret-header-auth"; import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets"; import { Button } from "@executor/react/components/button"; import { Input } from "@executor/react/components/input"; import { Label } from "@executor/react/components/label"; import { Spinner } from "@executor/react/components/spinner"; import { addGraphqlSource } from "./atoms"; import type { HeaderValue } from "../sdk/types"; type HeaderEntry = { name: string; prefix?: string; presetKey?: string; secretId: string | null; }; const initialHeader = (): HeaderEntry => ({ name: "Authorization", prefix: "Bearer ", presetKey: "bearer", secretId: null, }); export default function AddGraphqlSource(props: { onComplete: () => void; onCancel: () => void; initialUrl?: string; }) { const [endpoint, setEndpoint] = useState(props.initialUrl ?? ""); const [namespace, setNamespace] = useState(""); const [headers, setHeaders] = useState([initialHeader()]); const [adding, setAdding] = useState(false); const [addError, setAddError] = useState(null); const scopeId = useScope(); const doAdd = useAtomSet(addGraphqlSource, { mode: "promise" }); const secretList = useSecretPickerSecrets(); const headersValid = headers.every((header) => header.name.trim() && header.secretId); const canAdd = endpoint.trim().length > 0 && (headers.length === 0 || headersValid); const updateHeader = ( index: number, update: Partial<{ name: string; prefix?: string; presetKey?: string; secretId: string | null }>, ) => { setHeaders((current) => current.map((header, i) => (i === index ? { ...header, ...update } : header)), ); }; const removeHeader = (index: number) => { setHeaders((current) => current.filter((_, i) => i !== index)); }; const addHeader = () => { setHeaders((current) => [ ...current, { name: "", prefix: undefined, presetKey: undefined, secretId: null }, ]); }; const handleAdd = async () => { setAdding(true); setAddError(null); try { const headerMap: Record = {}; for (const header of headers) { const name = header.name.trim(); if (name && header.secretId) { headerMap[name] = { secretId: header.secretId, ...(header.prefix ? { prefix: header.prefix } : {}), }; } } await doAdd({ path: { scopeId }, payload: { endpoint: endpoint.trim(), namespace: namespace.trim() || undefined, ...(Object.keys(headerMap).length > 0 ? { headers: headerMap } : {}), }, }); props.onComplete(); } catch (e) { setAddError(e instanceof Error ? e.message : "Failed to add source"); setAdding(false); } }; return (

Add GraphQL Source

{/* Endpoint */}
setEndpoint((e.target as HTMLInputElement).value)} placeholder="https://api.example.com/graphql" className="font-mono text-sm" />

The endpoint will be introspected to discover available queries and mutations.

{/* Namespace */}
setNamespace((e.target as HTMLInputElement).value)} placeholder="my_api" className="font-mono text-sm" />

A prefix for the tool names. Derived from the endpoint hostname if not provided.

{/* Authentication */}

Secret-backed headers sent with every request, including introspection.

{headers.length > 0 && (
{headers.map((header, index) => ( updateHeader(index, update)} onSelectSecret={(secretId) => updateHeader(index, { secretId })} onRemove={() => removeHeader(index)} existingSecrets={secretList} /> ))}
)}
{/* Error */} {addError && (

{addError}

)} {/* Actions */}
); }