import { useState } from "react"; import { useAtomValue, useAtomSet } from "@effect/atom-react"; import * as Exit from "effect/Exit"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { graphqlSourceAtom, graphqlSourceBindingsAtom, setGraphqlSourceBinding, updateGraphqlSource, } from "./atoms"; import { connectionsAtom } from "@executor-js/react/api/atoms"; import { useScope, useScopeStack } from "@executor-js/react/api/scope-context"; import { connectionWriteKeys, sourceWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets"; import { HttpCredentialsEditor, serializeHttpCredentials, serializeScopedHttpCredentials, type HttpCredentialsState, } from "@executor-js/react/plugins/http-credentials"; import { effectiveCredentialBindingForScope, httpCredentialsFromConfiguredCredentialBindings, initialCredentialTargetScope, } from "@executor-js/react/plugins/credential-bindings"; import { slugifyNamespace, useSourceIdentity } from "@executor-js/react/plugins/source-identity"; import { useCredentialTargetScope } from "@executor-js/react/plugins/credential-target-scope"; import { Button } from "@executor-js/react/components/button"; import { FilterTabs } from "@executor-js/react/components/filter-tabs"; import { SourceOAuthConnectionControl } from "@executor-js/react/plugins/source-oauth-connection"; import { Badge } from "@executor-js/react/components/badge"; import { ScopeId } from "@executor-js/sdk/core"; import { GraphqlSourceFields } from "./GraphqlSourceFields"; import { GRAPHQL_OAUTH_CONNECTION_SLOT, type GraphqlCredentialInput, GraphqlSourceBindingInput, type GraphqlSourceBindingRef, } from "../sdk/types"; import type { StoredGraphqlSource } from "../sdk/store"; type EditableSource = StoredGraphqlSource; type AuthMode = "none" | "oauth2"; // --------------------------------------------------------------------------- // Edit form // --------------------------------------------------------------------------- function EditForm(props: { sourceId: string; initial: EditableSource; bindings: readonly GraphqlSourceBindingRef[]; onSave: () => void; }) { const displayScope = useScope(); const scopeStack = useScopeStack(); const sourceScope = ScopeId.make(props.initial.scope); const { credentialTargetScope, credentialScopeOptions } = useCredentialTargetScope({ sourceScope, initialTargetScope: initialCredentialTargetScope(sourceScope, props.bindings), }); const { credentialTargetScope: oauthCredentialTargetScope, setCredentialTargetScope: setOAuthCredentialTargetScope, } = useCredentialTargetScope({ sourceScope, initialTargetScope: initialCredentialTargetScope(sourceScope, props.bindings), }); const doUpdate = useAtomSet(updateGraphqlSource, { mode: "promiseExit" }); const setBinding = useAtomSet(setGraphqlSourceBinding, { mode: "promise" }); const secretList = useSecretPickerSecrets(); const connectionsResult = useAtomValue(connectionsAtom(displayScope)); const identity = useSourceIdentity({ fallbackName: props.initial.name, fallbackNamespace: props.initial.namespace, }); const [endpoint, setEndpoint] = useState(props.initial.endpoint); const [credentials, setCredentials] = useState(() => httpCredentialsFromConfiguredCredentialBindings({ headers: props.initial.headers, queryParams: props.initial.queryParams, bindings: props.bindings, }), ); const [authMode, setAuthMode] = useState(props.initial.auth.kind); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [credentialsDirty, setCredentialsDirty] = useState(false); const [authDirty, setAuthDirty] = useState(false); const identityDirty = identity.name.trim() !== props.initial.name.trim(); const metadataDirty = identityDirty || endpoint.trim() !== props.initial.endpoint.trim(); const dirty = metadataDirty || credentialsDirty || authDirty; const oauth2 = props.initial.auth.kind === "oauth2" ? props.initial.auth : null; const connections = AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : []; const scopeRanks = new Map(scopeStack.map((scope, index) => [scope.id, index] as const)); const connectionBinding = oauth2 ? effectiveCredentialBindingForScope( props.bindings, oauth2.connectionSlot, oauthCredentialTargetScope, scopeRanks, ) : null; const boundConnectionId = connectionBinding?.value.kind === "connection" ? connectionBinding.value.connectionId : null; const isConnected = boundConnectionId !== null && connections.some((connection) => connection.id === boundConnectionId); const oauthRequestCredentials = serializeHttpCredentials(credentials); const handleCredentialsChange = (next: HttpCredentialsState) => { setCredentials(next); setCredentialsDirty(true); }; const handleSave = async () => { setSaving(true); setError(null); const { headers, queryParams } = serializeScopedHttpCredentials( credentials, credentialTargetScope, ); const payload: { sourceScope: ScopeId; name?: string; endpoint?: string; headers?: Record; queryParams?: Record; credentialTargetScope?: ScopeId; auth?: { kind: "none" } | { kind: "oauth2"; connectionSlot: string }; } = { sourceScope, name: metadataDirty ? identity.name.trim() || undefined : undefined, endpoint: metadataDirty ? endpoint.trim() || undefined : undefined, }; if (credentialsDirty) { payload.headers = headers; payload.queryParams = queryParams as Record; payload.credentialTargetScope = credentialTargetScope; } if (authDirty) { payload.auth = authMode === "oauth2" ? { kind: "oauth2", connectionSlot: props.initial.auth.kind === "oauth2" ? props.initial.auth.connectionSlot : GRAPHQL_OAUTH_CONNECTION_SLOT, } : { kind: "none" }; payload.credentialTargetScope = credentialTargetScope; } const exit = await doUpdate({ params: { scopeId: displayScope, namespace: props.sourceId }, payload, reactivityKeys: sourceWriteKeys, }); if (Exit.isFailure(exit)) { setError("Failed to update source"); setSaving(false); return; } setCredentialsDirty(false); setAuthDirty(false); props.onSave(); setSaving(false); }; return (

Edit GraphQL Source

Update the endpoint and authentication headers for this source.

{props.sourceId}

GraphQL
{/* 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); setAuthDirty(true); }} />
{authMode === "oauth2" && (

OAuth sign-in is available from the source header after saving.

)}
{oauth2 && ( { await setBinding({ params: { scopeId: oauthCredentialTargetScope }, payload: new GraphqlSourceBindingInput({ sourceId: props.sourceId, sourceScope, scope: oauthCredentialTargetScope, slot: oauth2.connectionSlot, value: { kind: "connection", connectionId }, }), reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys], }); }} /> )} {error && (

{error}

)}
); } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export default function EditGraphqlSource(props: { sourceId: string; onSave: () => void }) { const scopeId = useScope(); const sourceResult = useAtomValue(graphqlSourceAtom(scopeId, props.sourceId)); const source = AsyncResult.isSuccess(sourceResult) && sourceResult.value ? sourceResult.value : null; const sourceScope = source ? ScopeId.make(source.scope) : scopeId; const bindingsResult = useAtomValue( graphqlSourceBindingsAtom(scopeId, props.sourceId, sourceScope), ); if (!AsyncResult.isSuccess(sourceResult) || !source || !AsyncResult.isSuccess(bindingsResult)) { return (

Edit GraphQL Source

Loading configuration…

); } return ( ); }