623 lines · 21.4 KB · TypeScript
RawDownload
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<unknown, unknown>, 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 (    <div className="space-y-2">      <div className="space-y-1">        <FieldLabel className="text-[11px]">{label}</FieldLabel>        {help && <p className="text-xs text-muted-foreground">{help}</p>}      </div>      <div className="flex items-center gap-2">        <div className="flex-1 min-w-0">          <CreatableSecretPicker            value={secretId}            onSelect={(id) => onSelect(id)}            secrets={secretList}            placeholder={placeholder}            suggestedId={props.suggestedSecretId}            secretLabel={label}            targetScope={props.targetScope}          />        </div>        {clearable && secretId && (          <Button variant="outline" onClick={() => onSelect(null)}>            Clear          </Button>        )}      </div>    </div>  );} 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 (    <img      alt=""      aria-hidden="true"      className={`${className} shrink-0 object-contain`}      decoding="async"      draggable={false}      loading="lazy"      src={src}    />  );} 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<GoogleAuthKind>("oauth2");  const [clientIdSecretId, setClientIdSecretId] = useState<string | null>(null);  const [clientSecretSecretId, setClientSecretSecretId] = useState<string | null>(null);  const [probe, setProbe] = useState<ProbeResult | null>(null);  const identity = useSourceIdentity({    fallbackName: probe?.name ?? selectedTemplate?.name ?? "",  });  const [oauthAuth, setOauthAuth] = useState<OAuthAuth | null>(null);  const [loadingProbe, setLoadingProbe] = useState(false);  const [adding, setAdding] = useState(false);  const [error, setError] = useState<string | null>(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 (    <div className="flex flex-1 flex-col gap-6">      <div>        <h1 className="text-xl font-semibold text-foreground">Add Google Discovery Source</h1>        <p className="mt-1 text-[13px] text-muted-foreground">          Connect a Google API from its Discovery document and register its methods as tools.        </p>      </div>       <FieldGroup>        <FieldSet>          <FieldLegend variant="label">Presets</FieldLegend>          <FieldDescription>Select a Google API to prefill the source.</FieldDescription>          <RadioGroup            value={selectedTemplateId}            onValueChange={(value) => {              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 (                <FieldLabel key={template.id} htmlFor={inputId}>                  <Field orientation="horizontal">                    <GoogleServiceIcon                      icon={template.icon}                      service={template.service}                      className="size-8"                    />                    <FieldContent>                      <FieldTitle>{template.name}</FieldTitle>                      <FieldDescription className="line-clamp-2">                        {template.summary}                      </FieldDescription>                    </FieldContent>                    <RadioGroupItem id={inputId} value={template.id} />                  </Field>                </FieldLabel>              );            })}          </RadioGroup>        </FieldSet>      </FieldGroup>       <CardStack>        <CardStackContent className="border-t-0">          <CardStackEntryField label="Discovery URL">            <div className="relative">              <Input                value={discoveryUrl}                onChange={(e) => {                  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 && (                <div className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2">                  <IOSSpinner className="size-4" />                </div>              )}            </div>          </CardStackEntryField>        </CardStackContent>      </CardStack>       <SourceIdentityFields        identity={identity}        namePlaceholder="Google Sheets"        namespacePlaceholder="google_sheets"      />       {probe && (        <section className="space-y-3 rounded-xl border border-border bg-card px-4 py-4">          <div className="flex items-center justify-between gap-3">            <div className="flex items-center gap-3">              <div className="flex size-9 shrink-0 items-center justify-center rounded-xl border border-border/60 bg-background/80 shadow-xs">                <GoogleServiceIcon                  icon={selectedTemplate?.icon}                  service={selectedTemplate?.service ?? probe.service}                  className="size-5"                />              </div>              <div>                <p className="text-sm font-semibold text-foreground">{probe.title ?? probe.name}</p>                <p className="mt-1 text-xs text-muted-foreground">                  {probe.service} · {probe.version}                </p>              </div>            </div>            <div className="flex gap-2">              <Badge variant="secondary">{probe.toolCount} tools</Badge>              <Badge variant="outline">{probe.scopes.length} scopes</Badge>            </div>          </div>        </section>      )}       <section className="space-y-2.5">        <div className="flex items-center justify-between gap-3">          <FieldLabel>Authentication</FieldLabel>

Lines 1–500 of 623.