import { useCallback, useState } from "react"; import { useAtomSet } from "@effect/atom-react"; import { useScope } from "@executor-js/react/api/scope-context"; import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { usePendingSources } from "@executor-js/react/api/optimistic"; import { HttpCredentialsEditor, httpCredentialsValid, serializeHttpCredentials, type HttpCredentialsState, } from "@executor-js/react/plugins/http-credentials"; import { displayNameFromUrl, slugifyNamespace, SourceIdentityFields, useSourceIdentity, } from "@executor-js/react/plugins/source-identity"; import { oauthCallbackUrl, oauthConnectionId, useOAuthPopupFlow, type OAuthCompletionPayload, } from "@executor-js/react/plugins/oauth-sign-in"; import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets"; import { Button } from "@executor-js/react/components/button"; import { FilterTabs } from "@executor-js/react/components/filter-tabs"; import { CardStack, CardStackContent, CardStackEntryField, } from "@executor-js/react/components/card-stack"; import { FloatActions } from "@executor-js/react/components/float-actions"; import { Input } from "@executor-js/react/components/input"; import { Spinner } from "@executor-js/react/components/spinner"; import { addGraphqlSource } from "./atoms"; import { initialGraphqlCredentials } from "./defaults"; import type { HeaderValue } from "../sdk/types"; type AuthMode = "none" | "oauth2"; export default function AddGraphqlSource(props: { onComplete: () => void; onCancel: () => void; initialUrl?: string; }) { const [endpoint, setEndpoint] = useState(props.initialUrl ?? ""); const identity = useSourceIdentity({ fallbackName: displayNameFromUrl(endpoint) ?? "", }); const [credentials, setCredentials] = useState(initialGraphqlCredentials); const [adding, setAdding] = useState(false); const [addError, setAddError] = useState(null); const [authMode, setAuthMode] = useState("none"); const [tokens, setTokens] = useState(null); const scopeId = useScope(); const doAdd = useAtomSet(addGraphqlSource, { mode: "promise" }); const { beginAdd } = usePendingSources(); const secretList = useSecretPickerSecrets(); const oauth = useOAuthPopupFlow({ popupName: "graphql-oauth", startErrorMessage: "Failed to start OAuth", }); const canAdd = endpoint.trim().length > 0 && httpCredentialsValid(credentials) && (authMode === "none" || tokens !== null) && !oauth.busy; const sourceIdentity = useCallback(() => { const trimmedEndpoint = endpoint.trim(); const namespace = slugifyNamespace(identity.namespace) || slugifyNamespace(displayNameFromUrl(trimmedEndpoint) ?? "") || "graphql"; const displayName = identity.name.trim() || displayNameFromUrl(trimmedEndpoint) || namespace; return { trimmedEndpoint, namespace, displayName }; }, [endpoint, identity.name, identity.namespace]); const handleOAuth = useCallback(async () => { if (!endpoint.trim() || !httpCredentialsValid(credentials)) return; setAddError(null); const { trimmedEndpoint, namespace, displayName } = sourceIdentity(); const { headers, queryParams } = serializeHttpCredentials(credentials); await oauth.start({ payload: { endpoint: trimmedEndpoint, ...(Object.keys(headers).length > 0 ? { headers } : {}), ...(Object.keys(queryParams).length > 0 ? { queryParams } : {}), redirectUrl: oauthCallbackUrl(), connectionId: oauthConnectionId({ pluginId: "graphql", namespace }), strategy: { kind: "dynamic-dcr" }, pluginId: "graphql", identityLabel: `${displayName} OAuth`, }, onSuccess: (result) => { setTokens({ connectionId: result.connectionId, expiresAt: result.expiresAt, scope: result.scope, }); }, onError: setAddError, }); }, [endpoint, credentials, oauth, sourceIdentity]); const handleAdd = async () => { setAdding(true); setAddError(null); const { headers: headerMap, queryParams } = serializeHttpCredentials(credentials); const { trimmedEndpoint, namespace, displayName } = sourceIdentity(); const placeholder = beginAdd({ id: namespace, name: displayName, kind: "graphql", url: trimmedEndpoint || undefined, }); try { await doAdd({ params: { scopeId }, payload: { endpoint: trimmedEndpoint, name: identity.name.trim() || undefined, namespace: slugifyNamespace(identity.namespace) || undefined, ...(Object.keys(headerMap).length > 0 ? { headers: headerMap } : {}), ...(Object.keys(queryParams).length > 0 ? { queryParams: queryParams as Record } : {}), ...(authMode === "oauth2" && tokens ? { auth: { kind: "oauth2" as const, connectionId: tokens.connectionId, }, } : {}), }, reactivityKeys: sourceWriteKeys, }); props.onComplete(); } catch (e) { setAddError(e instanceof Error ? e.message : "Failed to add source"); setAdding(false); } finally { placeholder.done(); } }; return (

Add GraphQL Source

setEndpoint((e.target as HTMLInputElement).value)} placeholder="https://api.example.com/graphql" className="font-mono text-sm" />
Authentication tabs={[ { value: "none", label: "None" }, { value: "oauth2", label: "OAuth" }, ]} value={authMode} onChange={(value) => { setAuthMode(value); setTokens(null); }} />
{authMode === "oauth2" && (
{tokens ? ( Authenticated ) : ( Sign in before adding so Executor can introspect the schema. )}
)}
{/* Error */} {addError && (

{addError}

)}
); }