import { useMemo, 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 } from "./atoms"; import { configureSource, connectionsAtom, setSourceCredentialBinding, } from "@executor-js/react/api/atoms"; import { useScope, useUserScope } 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 { httpCredentialsFromConfiguredCredentialBindings, serializeHttpCredentials, type HttpCredentialsState, } from "@executor-js/react/plugins/http-credentials"; import { slugifyNamespace, useSourceIdentity } from "@executor-js/react/plugins/source-identity"; import { useCredentialTargetScope } from "@executor-js/react/plugins/credential-target-scope"; import { useSourceCredentialBindingScopes, useSourceCredentialBindingWriter, } from "@executor-js/react/plugins/source-credential-bindings"; import { Button } from "@executor-js/react/components/button"; import { CardStack, CardStackContent, CardStackEntry, CardStackEntryContent, CardStackEntryDescription, CardStackEntryTitle, } from "@executor-js/react/components/card-stack"; import { FilterTabs } from "@executor-js/react/components/filter-tabs"; import { SourceOAuthConnectionControl, sourceOAuthConnectionUiState, } from "@executor-js/react/plugins/source-oauth-connection"; import { Badge } from "@executor-js/react/components/badge"; import { type CredentialBindingRef, ScopeId } from "@executor-js/sdk/shared"; import { SecretCredentialSlotBindings, secretCredentialSlotsFromHttpConfig, } from "@executor-js/react/plugins/credential-slot-bindings"; import { GraphqlSourceFields } from "./GraphqlSourceFields"; import type { GraphqlSourceAuthInput } 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 CredentialBindingRef[]; onSave: () => void; }) { const displayScope = useScope(); const userScope = useUserScope(); const sourceScope = ScopeId.make(props.initial.scope); const { credentialTargetScope: oauthCredentialTargetScope, setCredentialTargetScope: setOAuthCredentialTargetScope, } = useCredentialTargetScope({ sourceScope, initialTargetScope: userScope, }); const doConfigure = useAtomSet(configureSource, { mode: "promiseExit" }); const setConnectionBinding = useAtomSet(setSourceCredentialBinding, { mode: "promise" }); const secretList = useSecretPickerSecrets(); const connectionsResult = useAtomValue(connectionsAtom(userScope)); const identity = useSourceIdentity({ fallbackName: props.initial.name, fallbackNamespace: props.initial.namespace, }); const [endpoint, setEndpoint] = useState(props.initial.endpoint); const credentials = useMemo( () => httpCredentialsFromConfiguredCredentialBindings({ headers: props.initial.headers, queryParams: props.initial.queryParams, bindings: props.bindings, }), [props.bindings, props.initial.headers, props.initial.queryParams], ); const [authMode, setAuthMode] = useState(props.initial.auth.kind); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [authDirty, setAuthDirty] = useState(false); const { busyKey, setSecretBinding, clearBinding } = useSourceCredentialBindingWriter({ displayScope, source: { id: props.sourceId, scope: sourceScope }, onError: setError, }); const identityDirty = identity.name.trim() !== props.initial.name.trim(); const metadataDirty = identityDirty || endpoint.trim() !== props.initial.endpoint.trim(); const dirty = metadataDirty || authDirty; const oauth2 = props.initial.auth.kind === "oauth2" ? props.initial.auth : null; const connections = AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : []; const { credentialScopeOptions, secretBindingScopes, scopeRanks } = useSourceCredentialBindingScopes({ sourceScope }); const secretSlots = secretCredentialSlotsFromHttpConfig({ headers: props.initial.headers, queryParams: props.initial.queryParams, }); const oauthConnectionState = oauth2 ? sourceOAuthConnectionUiState({ bindings: props.bindings, connectionSlot: oauth2.connectionSlot, tokenScope: oauthCredentialTargetScope, scopeRanks, credentialScopeOptions, connections, }) : null; const oauthRequestCredentials = serializeHttpCredentials(credentials); const handleSave = async () => { setSaving(true); setError(null); const config: { name?: string; endpoint?: string; auth?: GraphqlSourceAuthInput; } = { name: metadataDirty ? identity.name.trim() || undefined : undefined, endpoint: metadataDirty ? endpoint.trim() || undefined : undefined, }; if (authDirty) { config.auth = authMode === "oauth2" ? { oauth2: {} } : { kind: "none" }; } const exit = await doConfigure({ params: { scopeId: displayScope }, payload: { source: { id: props.sourceId, scope: sourceScope }, scope: sourceScope, type: "graphql", config, }, reactivityKeys: sourceWriteKeys, }); if (Exit.isFailure(exit)) { setError("Failed to update source"); setSaving(false); return; } setAuthDirty(false); props.onSave(); setSaving(false); }; return (

Edit GraphQL Source

Update the endpoint and authentication headers for this source.

{props.sourceId}

GraphQL
{secretSlots.length > 0 && ( Request credentials Headers and query parameters sent with every GraphQL request. )} {/* 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 && oauthConnectionState && ( { await setConnectionBinding({ params: { scopeId: oauthCredentialTargetScope }, payload: { scope: oauthCredentialTargetScope, source: { id: props.sourceId, scope: sourceScope }, slotKey: oauth2.connectionSlot, value: { kind: "connection", connectionId }, }, reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys], }); }} signInLabel={oauthConnectionState.signInLabel} /> )} {error && (

{error}

)}
); } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export default function EditGraphqlSource(props: { sourceId: string; onSave: () => void }) { const scopeId = useScope(); const userScope = useUserScope(); 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(userScope, props.sourceId, sourceScope), ); if (!AsyncResult.isSuccess(sourceResult) || !source || !AsyncResult.isSuccess(bindingsResult)) { return (

Edit GraphQL Source

Loading configuration…

); } return ( ); }