import { useCallback, useEffect, useMemo, useRef, 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 { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { useScope, useUserScope } from "@executor-js/react/api/scope-context"; import type { SecretPickerSecret } from "@executor-js/react/plugins/secret-picker"; import { CreatableSecretPicker } from "@executor-js/react/plugins/secret-header-auth"; import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets"; import type { ScopeId } from "@executor-js/sdk"; import { Badge } from "@executor-js/react/components/badge"; import { Button } from "@executor-js/react/components/button"; import { CardStack, CardStackContent, CardStackEntryField, } from "@executor-js/react/components/card-stack"; import { SourceIdentityFields, slugifyNamespace, useSourceIdentity, } from "@executor-js/react/plugins/source-identity"; import { oauthCallbackUrl, oauthConnectionId, useOAuthPopupFlow, } from "@executor-js/react/plugins/oauth-sign-in"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@executor-js/react/components/collapsible"; import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle, } from "@executor-js/react/components/field"; import { FilterTabs } from "@executor-js/react/components/filter-tabs"; import { FloatActions } from "@executor-js/react/components/float-actions"; import { Input } from "@executor-js/react/components/input"; import { RadioGroup, RadioGroupItem } from "@executor-js/react/components/radio-group"; import { IOSSpinner, Spinner } from "@executor-js/react/components/spinner"; import { addGoogleDiscoverySourceOptimistic, probeGoogleDiscovery } from "./atoms"; import { GOOGLE_DISCOVERY_OAUTH_POPUP_NAME, googleDiscoveryOAuthStrategy } from "./oauth"; import { googleDiscoveryPresets, type GoogleDiscoveryPreset } from "../sdk/presets"; 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 GoogleAuthKind = "none" | "oauth2"; // --------------------------------------------------------------------------- // Client secret field with inline creation // --------------------------------------------------------------------------- function SecretBackedField(props: { label: string; help?: string; suggestedSecretId: string; secretId: string | null; onSelect: (secretId: string | null) => void; secretList: readonly SecretPickerSecret[]; placeholder: string; targetScope: ScopeId; clearable?: boolean; }) { const { label, help, secretId, onSelect, secretList, placeholder, clearable = true } = props; return (
{label} {help &&

{help}

}
onSelect(id)} secrets={secretList} placeholder={placeholder} suggestedId={props.suggestedSecretId} secretLabel={label} targetScope={props.targetScope} />
{clearable && secretId && ( )}
); } type GoogleDiscoveryTemplate = GoogleDiscoveryPreset & { readonly discoveryUrl: string; readonly service: string; readonly version: string; }; const GOOGLE_G_ICON = "https://fonts.gstatic.com/s/i/productlogos/googleg/v6/192px.svg"; function parseGoogleDiscoveryPreset(preset: GoogleDiscoveryPreset): GoogleDiscoveryTemplate { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor normalizes user-provided preset URLs try { const url = new URL(preset.url); const parts = url.pathname.split("/").filter(Boolean); const apisIndex = parts.indexOf("apis"); const service = apisIndex >= 0 ? parts[apisIndex + 1] : undefined; const version = apisIndex >= 0 ? parts[apisIndex + 2] : (url.searchParams.get("version") ?? undefined); return { ...preset, discoveryUrl: preset.url, service: service ?? url.hostname.replace(/\.googleapis\.com$/, ""), version: version ?? "", }; } catch { return { ...preset, discoveryUrl: preset.url, service: preset.id, version: "" }; } } const GOOGLE_DISCOVERY_TEMPLATES = googleDiscoveryPresets.map(parseGoogleDiscoveryPreset); const iconForService = (service: string): string | undefined => GOOGLE_DISCOVERY_TEMPLATES.find((template) => template.service === service)?.icon; function GoogleServiceIcon(props: { readonly icon?: string; readonly service?: string; readonly className?: string; }) { const { icon, service, className = "size-11" } = props; const src = icon ?? (service ? iconForService(service) : undefined) ?? GOOGLE_G_ICON; return ( ); } type ProbeOperation = { toolPath: string; method: string; pathTemplate: string; description: string | null; }; type ProbeResult = { name: string; title: string | null; service: string; version: string; toolCount: number; scopes: readonly string[]; operations: readonly ProbeOperation[]; }; type OAuthAuth = { kind: "oauth2"; connectionId: string; clientIdSecretId: string; clientSecretSecretId: string | null; scopes: string[]; }; export default function AddGoogleDiscoverySource(props: { readonly onComplete: () => void; readonly onCancel: () => void; readonly initialUrl?: string; readonly initialPreset?: string; }) { const defaultTemplate = GOOGLE_DISCOVERY_TEMPLATES.find((template) => template.id === props.initialPreset) ?? GOOGLE_DISCOVERY_TEMPLATES.find((template) => template.id === "google-sheets") ?? GOOGLE_DISCOVERY_TEMPLATES[0]!; const [discoveryUrl, setDiscoveryUrl] = useState( props.initialUrl ?? defaultTemplate.discoveryUrl, ); const [selectedTemplateId, setSelectedTemplateId] = useState( props.initialUrl ? "" : defaultTemplate.id, ); const selectedTemplate = GOOGLE_DISCOVERY_TEMPLATES.find((template) => template.id === selectedTemplateId) ?? null; const [authKind, setAuthKind] = useState("oauth2"); const [clientIdSecretId, setClientIdSecretId] = useState(null); const [clientSecretSecretId, setClientSecretSecretId] = useState(null); const [probe, setProbe] = useState(null); const identity = useSourceIdentity({ fallbackName: probe?.name ?? selectedTemplate?.name ?? "", }); const [oauthAuth, setOauthAuth] = useState(null); const [loadingProbe, setLoadingProbe] = useState(false); const [adding, setAdding] = useState(false); const [error, setError] = useState(null); const [showScopes, setShowScopes] = useState(false); const resolvedNamespace = slugifyNamespace(identity.namespace) || slugifyNamespace(probe?.name ?? selectedTemplate?.name ?? "") || "google"; const scopeId = useScope(); const userScopeId = useUserScope(); const doProbe = useAtomSet(probeGoogleDiscovery, { mode: "promiseExit" }); const doAdd = useAtomSet(addGoogleDiscoverySourceOptimistic(scopeId), { mode: "promiseExit", }); const secretList = useSecretPickerSecrets(); const oauth = useOAuthPopupFlow({ popupName: GOOGLE_DISCOVERY_OAUTH_POPUP_NAME, popupBlockedMessage: "OAuth popup was blocked", popupClosedMessage: "OAuth cancelled: popup was closed before completing the flow.", startErrorMessage: "Failed to start OAuth", }); const canUseOAuth = useMemo(() => (probe?.scopes.length ?? 0) > 0, [probe]); const applyTemplate = useCallback( (template: GoogleDiscoveryTemplate) => { setSelectedTemplateId(template.id); setDiscoveryUrl(template.discoveryUrl); identity.reset(); setClientSecretSecretId(null); setProbe(null); setOauthAuth(null); setError(null); setShowScopes(false); setAuthKind("oauth2"); }, [identity], ); const handleProbe = useCallback(async () => { setLoadingProbe(true); setError(null); setOauthAuth(null); setShowScopes(false); const exit = await doProbe({ params: { scopeId }, payload: { discoveryUrl: discoveryUrl.trim() }, }); if (Exit.isFailure(exit)) { setProbe(null); setLoadingProbe(false); setError(errorMessageFromExit(exit, "Failed to inspect discovery document")); return; } const result = exit.value; setProbe({ ...result, scopes: [...result.scopes], operations: [...result.operations], }); if (result.scopes.length === 0) { setAuthKind("none"); } setLoadingProbe(false); }, [discoveryUrl, doProbe, scopeId]); // Keep the latest handleProbe in a ref so the debounced effect can call it // without depending on its identity (which changes every render). const handleProbeRef = useRef(handleProbe); handleProbeRef.current = handleProbe; // Auto-probe whenever the discovery URL changes (debounced). Clearing the // previous probe in the onChange handler resets the preview so a new run // will be triggered. useEffect(() => { const trimmed = discoveryUrl.trim(); if (!trimmed) return; if (probe) return; const handle = setTimeout(() => { handleProbeRef.current(); }, 400); return () => clearTimeout(handle); }, [discoveryUrl, probe]); const handleStartOAuth = useCallback(async () => { if (!probe || !clientIdSecretId) return; setError(null); const scopes = [...probe.scopes]; await oauth.start({ payload: { endpoint: discoveryUrl.trim(), redirectUrl: oauthCallbackUrl(), connectionId: oauthConnectionId({ pluginId: "google-discovery", namespace: resolvedNamespace, }), tokenScope: userScopeId, identityLabel: `${identity.name.trim() || probe.title || probe.name} OAuth`, strategy: googleDiscoveryOAuthStrategy({ clientIdSecretId, clientSecretSecretId, scopes, }), pluginId: "google-discovery", }, onSuccess: (result) => { setOauthAuth({ kind: "oauth2", connectionId: result.connectionId, clientIdSecretId, clientSecretSecretId, scopes, }); setError(null); }, onError: setError, }); }, [ probe, discoveryUrl, identity.name, clientIdSecretId, clientSecretSecretId, resolvedNamespace, oauth, userScopeId, ]); const handleCancelOAuth = useCallback(() => { oauth.cancel(); }, [oauth]); const handleAdd = useCallback(async () => { if (!probe) return; setAdding(true); setError(null); const displayName = identity.name.trim() || probe.name; const namespace = resolvedNamespace; const exit = await doAdd({ params: { scopeId }, payload: { name: displayName, discoveryUrl: discoveryUrl.trim(), namespace, auth: authKind === "oauth2" && oauthAuth ? { kind: "oauth2" as const, connectionId: oauthAuth.connectionId, clientIdSecretId: oauthAuth.clientIdSecretId, clientSecretSecretId: oauthAuth.clientSecretSecretId, scopes: oauthAuth.scopes, } : { kind: "none" as const }, }, reactivityKeys: [...sourceWriteKeys], }); if (Exit.isFailure(exit)) { setError(errorMessageFromExit(exit, "Failed to add source")); setAdding(false); return; } props.onComplete(); }, [ probe, doAdd, identity, discoveryUrl, authKind, oauthAuth, props, scopeId, resolvedNamespace, ]); const addDisabled = !probe || adding || (authKind === "oauth2" && (!canUseOAuth || oauthAuth === null)); return (

Add Google Discovery Source

Connect a Google API from its Discovery document and register its methods as tools.

Presets Select a Google API to prefill the source. { const template = GOOGLE_DISCOVERY_TEMPLATES.find((t) => t.id === value); if (template) applyTemplate(template); }} className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3" > {GOOGLE_DISCOVERY_TEMPLATES.map((template) => { const inputId = `google-discovery-preset-${template.id}`; return ( {template.name} {template.summary} ); })}
{ setSelectedTemplateId(""); setDiscoveryUrl((e.target as HTMLInputElement).value); setProbe(null); setOauthAuth(null); setError(null); }} placeholder="https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest" className="w-full pr-9 font-mono text-sm" /> {loadingProbe && (
)}
{probe && (

{probe.title ?? probe.name}

{probe.service} · {probe.version}

{probe.toolCount} tools {probe.scopes.length} scopes
)}
Authentication tabs={[ { value: "none", label: "None" }, { value: "oauth2", label: "OAuth" }, ]} value={authKind} onChange={setAuthKind} />
{authKind === "oauth2" && (

{canUseOAuth ? `${probe?.scopes.length ?? 0} scopes will be requested from Google.` : "This API does not advertise OAuth scopes."}

{canUseOAuth && (probe?.scopes.length ?? 0) > 0 && ( )}
{oauth.busy && ( )}
    {(probe?.scopes ?? []).map((scope) => (
  • {scope}
  • ))}
{oauthAuth && (
Connected. Manage this connection from the Connections page.
)}
)}
{error && (
{error}
)}
); }