import { useCallback, useState } from "react"; import { useAtomSet } from "@effect/atom-react"; import * as Exit from "effect/Exit"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { useScope } from "@executor-js/react/api/scope-context"; import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { HttpCredentialsEditor, httpCredentialsValid, serializeConfigureHttpCredentials, serializeHttpCredentials, serializeTemplateHttpCredentials, type HttpCredentialsState, } from "@executor-js/react/plugins/http-credentials"; import { sourceDisplayNameFromUrl, slugifyNamespace, useSourceIdentity, } from "@executor-js/react/plugins/source-identity"; import { oauthCallbackUrl, oauthConnectionId, useOAuthPopupFlow, type OAuthCompletionPayload, } from "@executor-js/react/plugins/oauth-sign-in"; import { CredentialControlField, CredentialUsageRow, useCredentialTargetScope, } from "@executor-js/react/plugins/credential-target-scope"; 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 { FloatActions } from "@executor-js/react/components/float-actions"; import { Spinner } from "@executor-js/react/components/spinner"; import { addGraphqlSourceOptimistic } from "./atoms"; import { initialGraphqlCredentials } from "./defaults"; import { GraphqlSourceFields } from "./GraphqlSourceFields"; import type { GraphqlConfiguredValueInput, GraphqlCredentialInput } from "../sdk/types"; const ErrorMessage = Schema.Struct({ message: Schema.String }); const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage); const errorMessageFromExit = (exit: Exit.Exit, fallback: string): string => Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), { onNone: () => fallback, onSome: ({ message }) => message, }); 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: sourceDisplayNameFromUrl(endpoint, "GraphQL") ?? "", }); 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 { credentialTargetScope: requestCredentialTargetScope } = useCredentialTargetScope(); const { credentialTargetScope: oauthCredentialTargetScope, setCredentialTargetScope: setOAuthCredentialTargetScope, credentialScopeOptions, } = useCredentialTargetScope(); const doAdd = useAtomSet(addGraphqlSourceOptimistic(scopeId), { mode: "promiseExit", }); 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(sourceDisplayNameFromUrl(trimmedEndpoint, "GraphQL") ?? "") || "graphql"; const displayName = identity.name.trim() || sourceDisplayNameFromUrl(trimmedEndpoint, "GraphQL") || 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 }), tokenScope: oauthCredentialTargetScope, 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, oauthCredentialTargetScope]); const handleAdd = async () => { setAdding(true); setAddError(null); const { headers: templateHeaders, queryParams: templateQueryParams } = serializeTemplateHttpCredentials(credentials); const { headers: configureHeaders, queryParams: configureQueryParams } = serializeConfigureHttpCredentials(credentials, requestCredentialTargetScope); const hasInitialCredentials = Object.keys(configureHeaders).length > 0 || Object.keys(configureQueryParams).length > 0 || (authMode === "oauth2" && tokens); const { trimmedEndpoint, namespace, displayName } = sourceIdentity(); const exit = await doAdd({ params: { scopeId }, payload: { endpoint: trimmedEndpoint, name: displayName, namespace, ...(Object.keys(templateHeaders).length > 0 ? { headers: templateHeaders as Record } : {}), ...(Object.keys(templateQueryParams).length > 0 ? { queryParams: templateQueryParams as Record, } : {}), ...(hasInitialCredentials ? { credentials: { scope: requestCredentialTargetScope, ...(Object.keys(configureHeaders).length > 0 ? { headers: configureHeaders as Record } : {}), ...(Object.keys(configureQueryParams).length > 0 ? { queryParams: configureQueryParams as Record, } : {}), ...(authMode === "oauth2" && tokens ? { auth: { oauth2: { connection: { kind: "connection" as const, connectionId: tokens.connectionId, }, }, }, } : {}), }, } : {}), }, reactivityKeys: sourceWriteKeys, }); if (Exit.isFailure(exit)) { setAddError(errorMessageFromExit(exit, "Failed to add source")); setAdding(false); return; } props.onComplete(); }; return (

Add GraphQL Source

{/* Temporarily hidden while we revisit GraphQL OAuth discovery and UX. */}
Authentication tabs={[ { value: "none", label: "None" }, { value: "oauth2", label: "OAuth" }, ]} value={authMode} onChange={(value) => { setAuthMode(value); setTokens(null); }} />
{authMode === "oauth2" && ( { setOAuthCredentialTargetScope(targetScope); setTokens(null); }} label="Connection saved to" help="Choose who can use the OAuth connection." >
{tokens ? ( Authenticated ) : ( Not connected )}
)}
{/* Error */} {addError && (

{addError}

)}
); }