import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAtomSet } from "@effect/atom-react";
import { usePendingSources } from "@executor-js/react/api/optimistic";
import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";
import { useScope } 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 { 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 { addGoogleDiscoverySource, probeGoogleDiscovery } from "./atoms";
import { GOOGLE_DISCOVERY_OAUTH_POPUP_NAME, googleDiscoveryOAuthStrategy } from "./oauth";
import { googleDiscoveryPresets, type GoogleDiscoveryPreset } from "../sdk/presets";
type GoogleAuthKind = "none" | "oauth2";
// ---------------------------------------------------------------------------
// Client secret field with inline creation
// ---------------------------------------------------------------------------
function SecretBackedField(props: {
label: string;
suggestedSecretId: string;
secretId: string | null;
onSelect: (secretId: string | null) => void;
secretList: readonly SecretPickerSecret[];
placeholder: string;
clearable?: boolean;
}) {
const { label, secretId, onSelect, secretList, placeholder, clearable = true } = props;
return (
onSelect(id)}
secrets={secretList}
placeholder={placeholder}
suggestedId={props.suggestedSecretId}
secretLabel={label}
/>
{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 {
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 doProbe = useAtomSet(probeGoogleDiscovery, { mode: "promise" });
const doAdd = useAtomSet(addGoogleDiscoverySource, { mode: "promise" });
const { beginAdd } = usePendingSources();
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);
try {
const result = await doProbe({
params: { scopeId },
payload: { discoveryUrl: discoveryUrl.trim() },
});
setProbe({
...result,
scopes: [...result.scopes],
operations: [...result.operations],
});
if (result.scopes.length === 0) {
setAuthKind("none");
}
} catch (e) {
setProbe(null);
setError(e instanceof Error ? e.message : "Failed to inspect discovery document");
} finally {
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,
}),
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,
]);
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 placeholder = beginAdd({
id: namespace,
name: displayName,
kind: "google-discovery",
});
try {
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],
});
props.onComplete();
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to add source");
setAdding(false);
} finally {
placeholder.done();
}
}, [
probe,
doAdd,
identity,
discoveryUrl,
authKind,
oauthAuth,
props,
scopeId,
beginAdd,
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.
{
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}
)}
);
}