import { startTransition, useState, type ReactNode } from "react";import type { Source, SourceInspectionToolDetail,} from "@executor/react";import { SecretReferenceField, defineExecutorPluginHttpApiClient, Result, useAtomSet, useAtomValue, useExecutorMutation, useLocalInstallation, useSource,} from "@executor/react";import { Alert, Badge, Button, Card, IconPencil, Input, Label, Select, SourceToolDetailPanel, SourceToolExplorer, Textarea, parseSourceToolExplorerSearch, useSourcePluginRouteParams, type SourceToolExplorerSearch, useSourcePluginNavigation, useSourcePluginSearch,} from "@executor/react/plugins"; import { graphqlHttpApiExtension,} from "@executor/plugin-graphql-http";import { type GraphqlConnectInput, type GraphqlConnectionAuth,} from "@executor/plugin-graphql-shared"; const getGraphqlHttpClient = defineExecutorPluginHttpApiClient<"GraphqlReactHttpClient">()( "GraphqlReactHttpClient", [graphqlHttpApiExtension] as const,); type GraphqlToolRouteParams = { sourceId: string; toolPath?: string;}; const defaultGraphqlInput = (): GraphqlConnectInput => ({ name: "My GraphQL Source", endpoint: "https://example.com/graphql", defaultHeaders: null, auth: { kind: "none", },}); const DEFAULT_BEARER_HEADER_NAME = "Authorization";const DEFAULT_BEARER_PREFIX = "Bearer "; const presetString = ( search: Record<string, unknown>, key: string,): string | null => { const value = search[key]; return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;}; const firstSearchString = ( search: Record<string, unknown>, keys: ReadonlyArray<string>,): string | null => { for (const key of keys) { const value = presetString(search, key); if (value) { return value; } } return null;}; const graphqlInputFromSearch = ( search: Record<string, unknown>,): GraphqlConnectInput => { const defaults = defaultGraphqlInput(); return { ...defaults, name: presetString(search, "presetName") ?? defaults.name, endpoint: firstSearchString(search, [ "presetEndpoint", "endpoint", "inputUrl", "pastedUrl", "url", ]) ?? defaults.endpoint, };}; const parseStringMap = (value: string): Record<string, string> | null => { const trimmed = value.trim(); if (!trimmed) { return null; } const parsed = JSON.parse(trimmed) as unknown; if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("Headers must be a JSON object."); } const entries = Object.entries(parsed as Record<string, unknown>); if (!entries.every(([, entry]) => typeof entry === "string")) { throw new Error("All header values must be strings."); } return Object.fromEntries(entries as Array<[string, string]>);}; const stringifyStringMap = ( value: Record<string, string> | null | undefined,): string => !value || Object.keys(value).length === 0 ? "" : JSON.stringify(value, null, 2); const secretValue = (input: GraphqlConnectionAuth): string => input.kind === "bearer" ? JSON.stringify(input.tokenSecretRef) : ""; const bearerHeaderNameValue = (input: GraphqlConnectionAuth): string => input.kind === "bearer" ? input.headerName?.trim() || DEFAULT_BEARER_HEADER_NAME : DEFAULT_BEARER_HEADER_NAME; const bearerPrefixValue = (input: GraphqlConnectionAuth): string => input.kind === "bearer" ? input.prefix ?? DEFAULT_BEARER_PREFIX : DEFAULT_BEARER_PREFIX; const trimToNull = (value: string): string | null => { const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null;}; const authFromSecretValue = ( authKind: GraphqlConnectionAuth["kind"], value: string, headerName: string, prefix: string,): GraphqlConnectionAuth => { if (authKind === "none") { return { kind: "none", }; } if (!value) { throw new Error("Select a secret for bearer auth."); } return { kind: "bearer", tokenSecretRef: JSON.parse(value) as GraphqlConnectionAuth & { tokenSecretRef: never }["tokenSecretRef"], headerName: trimToNull(headerName), prefix: prefix === DEFAULT_BEARER_PREFIX ? null : prefix, };}; const graphqlToolRoutePath = (toolPath: string): string => `tool/${encodeURIComponent(toolPath)}`; const readGraphqlToolPath = (params: GraphqlToolRouteParams): string | null => { if (!params.toolPath || params.toolPath.length === 0) { return null; } try { return decodeURIComponent(params.toolPath); } catch { return params.toolPath; }}; function GraphqlSourceForm(props: { initialValue: GraphqlConnectInput; mode: "create" | "edit"; onSubmit: (input: GraphqlConnectInput) => Promise<void>;}) { const submitMutation = useExecutorMutation<GraphqlConnectInput, void>(props.onSubmit); const [name, setName] = useState(props.initialValue.name); const [endpoint, setEndpoint] = useState(props.initialValue.endpoint); const [headersText, setHeadersText] = useState( stringifyStringMap(props.initialValue.defaultHeaders), ); const [authKind, setAuthKind] = useState<GraphqlConnectionAuth["kind"]>( props.initialValue.auth.kind, ); const [secretRef, setSecretRef] = useState(secretValue(props.initialValue.auth)); const [authHeaderName, setAuthHeaderName] = useState( bearerHeaderNameValue(props.initialValue.auth), ); const [authPrefix, setAuthPrefix] = useState( bearerPrefixValue(props.initialValue.auth), ); const [error, setError] = useState<string | null>(null); return ( <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]"> <div className="grid gap-4"> <div className="grid gap-2"> <Label>Name</Label> <Input value={name} onChange={(event) => setName(event.target.value)} /> </div> <div className="grid gap-2"> <Label>Endpoint</Label> <Input value={endpoint} onChange={(event) => setEndpoint(event.target.value)} className="font-mono text-xs" /> </div> <div className="grid gap-2"> <Label>Default Headers</Label> <Textarea value={headersText} onChange={(event) => setHeadersText(event.target.value)} rows={3} placeholder='{"x-api-version":"2026-03-23"}' /> </div> <div className="grid gap-2"> <Label>Auth</Label> <Select value={authKind} onChange={(event) => setAuthKind(event.target.value as GraphqlConnectionAuth["kind"])} > <option value="none">None</option> <option value="bearer">Bearer Secret</option> </Select> </div> {authKind === "bearer" && ( <div className="space-y-4 rounded-lg border border-border bg-muted/20 p-4"> <SecretReferenceField label="Secret" value={secretRef} emptyLabel="Select a secret" draftNamePlaceholder="GraphQL bearer token" draftValuePlaceholder="Paste the token value" onChange={setSecretRef} /> <div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-2"> <Label>Header Name</Label> <Input value={authHeaderName} onChange={(event) => setAuthHeaderName(event.target.value)} placeholder={DEFAULT_BEARER_HEADER_NAME} className="font-mono text-xs" /> </div> <div className="grid gap-2"> <Label>Prefix</Label> <Input value={authPrefix} onChange={(event) => setAuthPrefix(event.target.value)} placeholder={DEFAULT_BEARER_PREFIX} className="font-mono text-xs" /> </div> </div> </div> )} </div> {error && ( <Alert variant="destructive"> {error} </Alert> )} <div className="flex items-center justify-end"> <Button type="button" onClick={() => { void (async () => { setError(null); try { await submitMutation.mutateAsync({ name: name.trim(), endpoint: endpoint.trim(), defaultHeaders: parseStringMap(headersText), auth: authFromSecretValue( authKind, secretRef, authHeaderName, authPrefix, ), }); } catch (cause) { setError(cause instanceof Error ? cause.message : "Failed saving GraphQL source."); } })(); }} disabled={submitMutation.status === "pending"} > {submitMutation.status === "pending" ? props.mode === "create" ? "Creating..." : "Saving..." : props.mode === "create" ? "Create Source" : "Save Changes"} </Button> </div> </div> );} export function GraphqlAddPage() { const navigation = useSourcePluginNavigation(); const initialValue = graphqlInputFromSearch(useSourcePluginSearch()); const installation = useLocalInstallation(); const client = getGraphqlHttpClient(); const createSource = useAtomSet( client.mutation("graphql", "createSource"), { mode: "promise" }, ); if (installation.status !== "ready") { return <div className="text-sm text-muted-foreground">Loading workspace...</div>; } return ( <div className="min-h-0 flex-1 overflow-y-auto"> <div className="mx-auto w-full max-w-3xl px-6 py-10"> <div className="mb-8"> <h1 className="font-display text-2xl tracking-tight text-foreground"> Add GraphQL Source </h1> <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> Connect a GraphQL endpoint and expose its operations as tools. </p> </div> <GraphqlSourceForm initialValue={initialValue} mode="create" onSubmit={async (input) => { const source = await createSource({ path: { workspaceId: installation.data.scopeId, }, payload: input, reactivityKeys: { sources: [installation.data.scopeId], }, }); startTransition(() => { void navigation.detail(source.id, { tab: "model", }); }); }} /> </div> </div> );} export function GraphqlEditPage(props: { source: Source;}) { const navigation = useSourcePluginNavigation(); const installation = useLocalInstallation(); const client = getGraphqlHttpClient(); const configResult = useAtomValue( installation.status === "ready" ? client.query("graphql", "getSourceConfig", { path: { workspaceId: installation.data.scopeId, sourceId: props.source.id, }, reactivityKeys: { source: [installation.data.scopeId, props.source.id], }, timeToLive: "30 seconds", }) : client.query("local", "installation", { timeToLive: "1 second", }) as never, ); const updateSource = useAtomSet( client.mutation("graphql", "updateSource"), { mode: "promise" }, ); if (installation.status !== "ready") { return <div className="text-sm text-muted-foreground">Loading workspace...</div>; } if (Result.isFailure(configResult)) { return ( <Alert variant="destructive"> Failed loading source configuration. </Alert> ); } if (!Result.isSuccess(configResult)) { return <div className="text-sm text-muted-foreground">Loading configuration...</div>; } return ( <div className="min-h-0 flex-1 overflow-y-auto"> <div className="mx-auto w-full max-w-3xl px-6 py-10"> <div className="mb-8"> <h1 className="font-display text-2xl tracking-tight text-foreground"> Edit GraphQL Source </h1> <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> Update the connection settings for {props.source.name}. </p> </div> <GraphqlSourceForm initialValue={configResult.value} mode="edit" onSubmit={async (input) => { const source = await updateSource({ path: { workspaceId: installation.data.scopeId, sourceId: props.source.id, }, payload: input, reactivityKeys: { sources: [installation.data.scopeId], source: [installation.data.scopeId, props.source.id], sourceInspection: [installation.data.scopeId, props.source.id], sourceInspectionTool: [installation.data.scopeId, props.source.id], sourceDiscovery: [installation.data.scopeId, props.source.id], }, }); startTransition(() => { void navigation.detail(source.id, { tab: "model", }); }); }} /> </div> </div> );} function GraphqlDetailExplorer(props: { source: Source; selectedToolPath: string | null;}) { const navigation = useSourcePluginNavigation(); const search = parseSourceToolExplorerSearch( useSourcePluginSearch(), ) satisfies SourceToolExplorerSearch; const installation = useLocalInstallation(); const client = getGraphqlHttpClient(); const removeSource = useAtomSet( client.mutation("graphql", "removeSource"), { mode: "promise" }, ); const configResult = useAtomValue( installation.status === "ready" ? client.query("graphql", "getSourceConfig", { path: { workspaceId: installation.data.scopeId, sourceId: props.source.id, }, reactivityKeys: { source: [installation.data.scopeId, props.source.id], }, timeToLive: "30 seconds", }) : client.query("local", "installation", { timeToLive: "1 second",Lines 1–500 of 833. Load more
import { startTransition, useState, type ReactNode } from "react"; import type { Source, SourceInspectionToolDetail, } from "@executor/react"; import { SecretReferenceField, defineExecutorPluginHttpApiClient, Result, useAtomSet, useAtomValue, useExecutorMutation, useLocalInstallation, useSource, } from "@executor/react"; import { Alert, Badge, Button, Card, IconPencil, Input, Label, Select, SourceToolDetailPanel, SourceToolExplorer, Textarea, parseSourceToolExplorerSearch, useSourcePluginRouteParams, type SourceToolExplorerSearch, useSourcePluginNavigation, useSourcePluginSearch, } from "@executor/react/plugins"; import { graphqlHttpApiExtension, } from "@executor/plugin-graphql-http"; import { type GraphqlConnectInput, type GraphqlConnectionAuth, } from "@executor/plugin-graphql-shared"; const getGraphqlHttpClient = defineExecutorPluginHttpApiClient<"GraphqlReactHttpClient">()( "GraphqlReactHttpClient", [graphqlHttpApiExtension] as const, ); type GraphqlToolRouteParams = { sourceId: string; toolPath?: string; }; const defaultGraphqlInput = (): GraphqlConnectInput => ({ name: "My GraphQL Source", endpoint: "https://example.com/graphql", defaultHeaders: null, auth: { kind: "none", }, }); const DEFAULT_BEARER_HEADER_NAME = "Authorization"; const DEFAULT_BEARER_PREFIX = "Bearer "; const presetString = ( search: Record<string, unknown>, key: string, ): string | null => { const value = search[key]; return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; }; const firstSearchString = ( search: Record<string, unknown>, keys: ReadonlyArray<string>, ): string | null => { for (const key of keys) { const value = presetString(search, key); if (value) { return value; } } return null; }; const graphqlInputFromSearch = ( search: Record<string, unknown>, ): GraphqlConnectInput => { const defaults = defaultGraphqlInput(); return { ...defaults, name: presetString(search, "presetName") ?? defaults.name, endpoint: firstSearchString(search, [ "presetEndpoint", "endpoint", "inputUrl", "pastedUrl", "url", ]) ?? defaults.endpoint, }; }; const parseStringMap = (value: string): Record<string, string> | null => { const trimmed = value.trim(); if (!trimmed) { return null; } const parsed = JSON.parse(trimmed) as unknown; if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("Headers must be a JSON object."); } const entries = Object.entries(parsed as Record<string, unknown>); if (!entries.every(([, entry]) => typeof entry === "string")) { throw new Error("All header values must be strings."); } return Object.fromEntries(entries as Array<[string, string]>); }; const stringifyStringMap = ( value: Record<string, string> | null | undefined, ): string => !value || Object.keys(value).length === 0 ? "" : JSON.stringify(value, null, 2); const secretValue = (input: GraphqlConnectionAuth): string => input.kind === "bearer" ? JSON.stringify(input.tokenSecretRef) : ""; const bearerHeaderNameValue = (input: GraphqlConnectionAuth): string => input.kind === "bearer" ? input.headerName?.trim() || DEFAULT_BEARER_HEADER_NAME : DEFAULT_BEARER_HEADER_NAME; const bearerPrefixValue = (input: GraphqlConnectionAuth): string => input.kind === "bearer" ? input.prefix ?? DEFAULT_BEARER_PREFIX : DEFAULT_BEARER_PREFIX; const trimToNull = (value: string): string | null => { const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; }; const authFromSecretValue = ( authKind: GraphqlConnectionAuth["kind"], value: string, headerName: string, prefix: string, ): GraphqlConnectionAuth => { if (authKind === "none") { return { kind: "none", }; } if (!value) { throw new Error("Select a secret for bearer auth."); } return { kind: "bearer", tokenSecretRef: JSON.parse(value) as GraphqlConnectionAuth & { tokenSecretRef: never }["tokenSecretRef"], headerName: trimToNull(headerName), prefix: prefix === DEFAULT_BEARER_PREFIX ? null : prefix, }; }; const graphqlToolRoutePath = (toolPath: string): string => `tool/${encodeURIComponent(toolPath)}`; const readGraphqlToolPath = (params: GraphqlToolRouteParams): string | null => { if (!params.toolPath || params.toolPath.length === 0) { return null; } try { return decodeURIComponent(params.toolPath); } catch { return params.toolPath; } }; function GraphqlSourceForm(props: { initialValue: GraphqlConnectInput; mode: "create" | "edit"; onSubmit: (input: GraphqlConnectInput) => Promise<void>; }) { const submitMutation = useExecutorMutation<GraphqlConnectInput, void>(props.onSubmit); const [name, setName] = useState(props.initialValue.name); const [endpoint, setEndpoint] = useState(props.initialValue.endpoint); const [headersText, setHeadersText] = useState( stringifyStringMap(props.initialValue.defaultHeaders), ); const [authKind, setAuthKind] = useState<GraphqlConnectionAuth["kind"]>( props.initialValue.auth.kind, ); const [secretRef, setSecretRef] = useState(secretValue(props.initialValue.auth)); const [authHeaderName, setAuthHeaderName] = useState( bearerHeaderNameValue(props.initialValue.auth), ); const [authPrefix, setAuthPrefix] = useState( bearerPrefixValue(props.initialValue.auth), ); const [error, setError] = useState<string | null>(null); return ( <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]"> <div className="grid gap-4"> <div className="grid gap-2"> <Label>Name</Label> <Input value={name} onChange={(event) => setName(event.target.value)} /> </div> <div className="grid gap-2"> <Label>Endpoint</Label> <Input value={endpoint} onChange={(event) => setEndpoint(event.target.value)} className="font-mono text-xs" /> </div> <div className="grid gap-2"> <Label>Default Headers</Label> <Textarea value={headersText} onChange={(event) => setHeadersText(event.target.value)} rows={3} placeholder='{"x-api-version":"2026-03-23"}' /> </div> <div className="grid gap-2"> <Label>Auth</Label> <Select value={authKind} onChange={(event) => setAuthKind(event.target.value as GraphqlConnectionAuth["kind"])} > <option value="none">None</option> <option value="bearer">Bearer Secret</option> </Select> </div> {authKind === "bearer" && ( <div className="space-y-4 rounded-lg border border-border bg-muted/20 p-4"> <SecretReferenceField label="Secret" value={secretRef} emptyLabel="Select a secret" draftNamePlaceholder="GraphQL bearer token" draftValuePlaceholder="Paste the token value" onChange={setSecretRef} /> <div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-2"> <Label>Header Name</Label> <Input value={authHeaderName} onChange={(event) => setAuthHeaderName(event.target.value)} placeholder={DEFAULT_BEARER_HEADER_NAME} className="font-mono text-xs" /> </div> <div className="grid gap-2"> <Label>Prefix</Label> <Input value={authPrefix} onChange={(event) => setAuthPrefix(event.target.value)} placeholder={DEFAULT_BEARER_PREFIX} className="font-mono text-xs" /> </div> </div> </div> )} </div> {error && ( <Alert variant="destructive"> {error} </Alert> )} <div className="flex items-center justify-end"> <Button type="button" onClick={() => { void (async () => { setError(null); try { await submitMutation.mutateAsync({ name: name.trim(), endpoint: endpoint.trim(), defaultHeaders: parseStringMap(headersText), auth: authFromSecretValue( authKind, secretRef, authHeaderName, authPrefix, ), }); } catch (cause) { setError(cause instanceof Error ? cause.message : "Failed saving GraphQL source."); } })(); }} disabled={submitMutation.status === "pending"} > {submitMutation.status === "pending" ? props.mode === "create" ? "Creating..." : "Saving..." : props.mode === "create" ? "Create Source" : "Save Changes"} </Button> </div> </div> ); } export function GraphqlAddPage() { const navigation = useSourcePluginNavigation(); const initialValue = graphqlInputFromSearch(useSourcePluginSearch()); const installation = useLocalInstallation(); const client = getGraphqlHttpClient(); const createSource = useAtomSet( client.mutation("graphql", "createSource"), { mode: "promise" }, ); if (installation.status !== "ready") { return <div className="text-sm text-muted-foreground">Loading workspace...</div>; } return ( <div className="min-h-0 flex-1 overflow-y-auto"> <div className="mx-auto w-full max-w-3xl px-6 py-10"> <div className="mb-8"> <h1 className="font-display text-2xl tracking-tight text-foreground"> Add GraphQL Source </h1> <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> Connect a GraphQL endpoint and expose its operations as tools. </p> </div> <GraphqlSourceForm initialValue={initialValue} mode="create" onSubmit={async (input) => { const source = await createSource({ path: { workspaceId: installation.data.scopeId, }, payload: input, reactivityKeys: { sources: [installation.data.scopeId], }, }); startTransition(() => { void navigation.detail(source.id, { tab: "model", }); }); }} /> </div> </div> ); } export function GraphqlEditPage(props: { source: Source; }) { const navigation = useSourcePluginNavigation(); const installation = useLocalInstallation(); const client = getGraphqlHttpClient(); const configResult = useAtomValue( installation.status === "ready" ? client.query("graphql", "getSourceConfig", { path: { workspaceId: installation.data.scopeId, sourceId: props.source.id, }, reactivityKeys: { source: [installation.data.scopeId, props.source.id], }, timeToLive: "30 seconds", }) : client.query("local", "installation", { timeToLive: "1 second", }) as never, ); const updateSource = useAtomSet( client.mutation("graphql", "updateSource"), { mode: "promise" }, ); if (installation.status !== "ready") { return <div className="text-sm text-muted-foreground">Loading workspace...</div>; } if (Result.isFailure(configResult)) { return ( <Alert variant="destructive"> Failed loading source configuration. </Alert> ); } if (!Result.isSuccess(configResult)) { return <div className="text-sm text-muted-foreground">Loading configuration...</div>; } return ( <div className="min-h-0 flex-1 overflow-y-auto"> <div className="mx-auto w-full max-w-3xl px-6 py-10"> <div className="mb-8"> <h1 className="font-display text-2xl tracking-tight text-foreground"> Edit GraphQL Source </h1> <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> Update the connection settings for {props.source.name}. </p> </div> <GraphqlSourceForm initialValue={configResult.value} mode="edit" onSubmit={async (input) => { const source = await updateSource({ path: { workspaceId: installation.data.scopeId, sourceId: props.source.id, }, payload: input, reactivityKeys: { sources: [installation.data.scopeId], source: [installation.data.scopeId, props.source.id], sourceInspection: [installation.data.scopeId, props.source.id], sourceInspectionTool: [installation.data.scopeId, props.source.id], sourceDiscovery: [installation.data.scopeId, props.source.id], }, }); startTransition(() => { void navigation.detail(source.id, { tab: "model", }); }); }} /> </div> </div> ); } function GraphqlDetailExplorer(props: { source: Source; selectedToolPath: string | null; }) { const navigation = useSourcePluginNavigation(); const search = parseSourceToolExplorerSearch( useSourcePluginSearch(), ) satisfies SourceToolExplorerSearch; const installation = useLocalInstallation(); const client = getGraphqlHttpClient(); const removeSource = useAtomSet( client.mutation("graphql", "removeSource"), { mode: "promise" }, ); const configResult = useAtomValue( installation.status === "ready" ? client.query("graphql", "getSourceConfig", { path: { workspaceId: installation.data.scopeId, sourceId: props.source.id, }, reactivityKeys: { source: [installation.data.scopeId, props.source.id], }, timeToLive: "30 seconds", }) : client.query("local", "installation", { timeToLive: "1 second",