814 lines · 27.4 KB · TypeScript
RawDownload
import { useCallback, useEffect, useMemo, useRef, useState } from "react";import { useAtomSet, useAtomValue, useAtomRefresh, Result } from "@effect-atom/atom-react"; import {  useScope,  SecretPicker,  secretsAtom,  setSecret,  type SecretPickerSecret,} from "@executor/react";import { SecretId } from "@executor/sdk";import { Badge } from "@executor/ui/components/badge";import { Button } from "@executor/ui/components/button";import {  Collapsible,  CollapsibleContent,  CollapsibleTrigger,} from "@executor/ui/components/collapsible";import { Input } from "@executor/ui/components/input";import { Label } from "@executor/ui/components/label";import { RadioGroup, RadioGroupItem } from "@executor/ui/components/radio-group";import { Spinner } from "@executor/ui/components/spinner";import {  addGoogleDiscoverySource,  probeGoogleDiscovery,  startGoogleDiscoveryOAuth,} from "./atoms"; // ---------------------------------------------------------------------------// Inline secret creation// --------------------------------------------------------------------------- function InlineCreateSecret(props: {  headerName: string;  suggestedId: string;  onCreated: (secretId: string) => void;  onCancel: () => void;}) {  const [secretId, setSecretIdValue] = useState(props.suggestedId);  const [secretName, setSecretName] = useState(props.headerName);  const [secretValue, setSecretValue] = useState("");  const [saving, setSaving] = useState(false);  const [error, setError] = useState<string | null>(null);  const scopeId = useScope();  const doSet = useAtomSet(setSecret, { mode: "promise" });  const refreshSecrets = useAtomRefresh(secretsAtom(scopeId));   const handleSave = async () => {    if (!secretId.trim() || !secretValue.trim()) return;    setSaving(true);    setError(null);    try {      await doSet({        path: { scopeId },        payload: {          id: SecretId.make(secretId.trim()),          name: secretName.trim() || secretId.trim(),          value: secretValue.trim(),          purpose: `Google OAuth: ${props.headerName}`,        },      });      refreshSecrets();      props.onCreated(secretId.trim());    } catch (e) {      setError(e instanceof Error ? e.message : "Failed to save secret");      setSaving(false);    }  };   return (    <div className="rounded-lg border border-primary/20 bg-primary/[0.02] p-3 space-y-2.5">      <p className="text-[11px] font-semibold text-primary tracking-wide uppercase">New secret</p>      <div className="grid grid-cols-2 gap-2">        <div className="space-y-1">          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">ID</Label>          <Input            value={secretId}            onChange={(e) => setSecretIdValue((e.target as HTMLInputElement).value)}            placeholder="google-client-secret"            className="h-8 text-xs font-mono"          />        </div>        <div className="space-y-1">          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">Label</Label>          <Input            value={secretName}            onChange={(e) => setSecretName((e.target as HTMLInputElement).value)}            placeholder="Client Secret"            className="h-8 text-xs"          />        </div>      </div>      <div className="space-y-1">        <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">Value</Label>        <Input          type="password"          value={secretValue}          onChange={(e) => setSecretValue((e.target as HTMLInputElement).value)}          placeholder="paste your client secret…"          className="h-8 text-xs font-mono"        />      </div>      {error && <p className="text-[11px] text-destructive">{error}</p>}      <div className="flex gap-1.5 pt-0.5">        <Button variant="outline" size="xs" onClick={props.onCancel}>          Cancel        </Button>        <Button          size="xs"          onClick={handleSave}          disabled={!secretId.trim() || !secretValue.trim() || saving}        >          {saving ? "Saving…" : "Create & use"}        </Button>      </div>    </div>  );} // ---------------------------------------------------------------------------// Client secret field with inline creation// --------------------------------------------------------------------------- function ClientSecretField(props: {  clientSecretSecretId: string | null;  onSelect: (secretId: string | null) => void;  secretList: readonly SecretPickerSecret[];}) {  const [creating, setCreating] = useState(false);  const { clientSecretSecretId, onSelect, secretList } = props;   if (creating) {    return (      <div className="space-y-2">        <Label>OAuth Client Secret</Label>        <InlineCreateSecret          headerName="Client Secret"          suggestedId="google-oauth-client-secret"          onCreated={(id) => {            onSelect(id);            setCreating(false);          }}          onCancel={() => setCreating(false)}        />      </div>    );  }   return (    <div className="space-y-2">      <Label>OAuth Client Secret</Label>      <div className="flex items-center gap-2">        <div className="flex-1 min-w-0">          <SecretPicker            value={clientSecretSecretId}            onSelect={onSelect}            secrets={secretList}            placeholder="Optional for confidential clients"          />        </div>        <Button          variant="outline"          size="sm"          className="shrink-0"          onClick={() => setCreating(true)}        >          + New        </Button>        {clientSecretSecretId && (          <Button            variant="outline"            onClick={() => onSelect(null)}          >            Clear          </Button>        )}      </div>    </div>  );} type GoogleDiscoveryTemplate = {  id: string;  name: string;  summary: string;  service: string;  version: string;  discoveryUrl: string;}; const defaultGoogleDiscoveryUrl = (service: string, version: string): string =>  `https://www.googleapis.com/discovery/v1/apis/${service}/${version}/rest`; const googleDiscoveryTemplate = (  template: GoogleDiscoveryTemplate,): GoogleDiscoveryTemplate => ({  ...template,  discoveryUrl:    template.discoveryUrl || defaultGoogleDiscoveryUrl(template.service, template.version),}); const GOOGLE_DISCOVERY_TEMPLATES: readonly GoogleDiscoveryTemplate[] = [  googleDiscoveryTemplate({    id: "google-calendar",    name: "Google Calendar",    summary: "Calendars, events, ACLs, and scheduling workflows.",    service: "calendar",    version: "v3",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",  }),  googleDiscoveryTemplate({    id: "google-drive",    name: "Google Drive",    summary: "Files, folders, permissions, comments, and shared drives.",    service: "drive",    version: "v3",    discoveryUrl: defaultGoogleDiscoveryUrl("drive", "v3"),  }),  googleDiscoveryTemplate({    id: "google-gmail",    name: "Gmail",    summary: "Messages, threads, labels, drafts, and mailbox automation.",    service: "gmail",    version: "v1",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest",  }),  googleDiscoveryTemplate({    id: "google-docs",    name: "Google Docs",    summary: "Documents, structural edits, text ranges, and formatting.",    service: "docs",    version: "v1",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/docs/v1/rest",  }),  googleDiscoveryTemplate({    id: "google-sheets",    name: "Google Sheets",    summary: "Spreadsheets, values, ranges, formatting, and batch updates.",    service: "sheets",    version: "v4",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest",  }),  googleDiscoveryTemplate({    id: "google-slides",    name: "Google Slides",    summary: "Presentations, slides, page elements, and deck updates.",    service: "slides",    version: "v1",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/slides/v1/rest",  }),  googleDiscoveryTemplate({    id: "google-forms",    name: "Google Forms",    summary: "Forms, questions, responses, quizzes, and form metadata.",    service: "forms",    version: "v1",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/forms/v1/rest",  }),  googleDiscoveryTemplate({    id: "google-people",    name: "Google People",    summary: "Contacts, profiles, directory people, and contact groups.",    service: "people",    version: "v1",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/people/v1/rest",  }),  googleDiscoveryTemplate({    id: "google-tasks",    name: "Google Tasks",    summary: "Task lists, task items, notes, and due dates.",    service: "tasks",    version: "v1",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/tasks/v1/rest",  }),  googleDiscoveryTemplate({    id: "google-chat",    name: "Google Chat",    summary: "Spaces, messages, members, reactions, and chat workflows.",    service: "chat",    version: "v1",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/chat/v1/rest",  }),  googleDiscoveryTemplate({    id: "google-bigquery",    name: "Google BigQuery",    summary: "Datasets, tables, jobs, routines, and analytics workflows.",    service: "bigquery",    version: "v2",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/bigquery/v2/rest",  }),  googleDiscoveryTemplate({    id: "google-youtube",    name: "YouTube Data",    summary: "Channels, playlists, videos, comments, captions, and uploads.",    service: "youtube",    version: "v3",    discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest",  }),]; const GOOGLE_SERVICE_ICON_URLS: Record<string, string> = {  calendar: "https://fonts.gstatic.com/s/i/productlogos/calendar_2020q4/v8/192px.svg",  drive: "https://fonts.gstatic.com/s/i/productlogos/drive_2020q4/v8/192px.svg",  gmail: "https://fonts.gstatic.com/s/i/productlogos/gmail_2020q4/v8/web-96dp/logo_gmail_2020q4_color_2x_web_96dp.png",  docs: "https://fonts.gstatic.com/s/i/productlogos/docs_2020q4/v12/192px.svg",  sheets: "https://fonts.gstatic.com/s/i/productlogos/sheets_2020q4/v8/192px.svg",  slides: "https://fonts.gstatic.com/s/i/productlogos/slides_2020q4/v12/192px.svg",  forms: "https://fonts.gstatic.com/s/i/productlogos/forms_2020q4/v6/192px.svg",  people: "https://fonts.gstatic.com/s/i/productlogos/contacts/v9/192px.svg",  tasks: "https://fonts.gstatic.com/s/i/productlogos/tasks/v10/192px.svg",  chat: "https://fonts.gstatic.com/s/i/productlogos/chat_2020q4/v8/192px.svg",  bigquery: "https://fonts.gstatic.com/s/i/productlogos/cloud/v8/192px.svg",  youtube: "https://fonts.gstatic.com/s/i/productlogos/youtube/v9/192px.svg",}; function GoogleServiceIcon(props: {  readonly service: string;  readonly className?: string;}) {  const { service, className = "size-11" } = props;  const src = GOOGLE_SERVICE_ICON_URLS[service] ?? GOOGLE_SERVICE_ICON_URLS.bigquery;   return (    <img      alt=""      aria-hidden="true"      className={`${className} shrink-0 object-contain`}      decoding="async"      draggable={false}      loading="lazy"      src={src}    />  );} type ProbeResult = {  name: string;  title: string | null;  service: string;  version: string;  toolCount: number;  scopes: readonly string[];}; type OAuthAuth = {  kind: "oauth2";  clientId: string;  clientSecretSecretId: string | null;  accessTokenSecretId: string;  refreshTokenSecretId: string | null;  tokenType: string;  expiresAt: number | null;  scope: string | null;  scopes: string[];}; type OAuthPopupResult =  | ({      type: "executor:oauth-result";      ok: true;      sessionId: string;    } & OAuthAuth)  | {      type: "executor:oauth-result";      ok: false;      sessionId: null;      error: string;    }; function openOAuthPopup(  url: string,  onResult: (data: OAuthPopupResult) => void,  onClosed?: () => void,): void {  const w = 640;  const h = 760;  const left = window.screenX + (window.outerWidth - w) / 2;  const top = window.screenY + (window.outerHeight - h) / 2;  const popup = window.open(    url,    "google-discovery-oauth",    `width=${w},height=${h},left=${left},top=${top},popup=1`,  );   let settled = false;  const settle = () => {    settled = true;    window.removeEventListener("message", onMessage);  };   const onMessage = (event: MessageEvent) => {    if (      event.origin === window.location.origin &&      event.data?.type === "executor:oauth-result" &&      !settled    ) {      settle();      onResult(event.data as OAuthPopupResult);    }  };  window.addEventListener("message", onMessage);   if (popup) {    const interval = window.setInterval(() => {      if (popup.closed) {        window.clearInterval(interval);        if (!settled) {          settle();          onClosed?.();        }      }    }, 500);  }} export default function AddGoogleDiscoverySource(props: {  readonly onComplete: () => void;  readonly onCancel: () => void;  readonly initialUrl?: string;}) {  const defaultTemplate =    GOOGLE_DISCOVERY_TEMPLATES.find((template) => template.id === "google-sheets")    ?? GOOGLE_DISCOVERY_TEMPLATES[0]!;  const [discoveryUrl, setDiscoveryUrl] = useState(    props.initialUrl ?? defaultTemplate.discoveryUrl,  );  const [name, setName] = useState(props.initialUrl ? "" : defaultTemplate.name);  const [selectedTemplateId, setSelectedTemplateId] = useState(props.initialUrl ? "" : defaultTemplate.id);  const selectedTemplate =    GOOGLE_DISCOVERY_TEMPLATES.find((template) => template.id === selectedTemplateId)    ?? null;  const [authKind, setAuthKind] = useState<"none" | "oauth2">("oauth2");  const [clientId, setClientId] = useState("");  const [clientSecretSecretId, setClientSecretSecretId] = useState<string | null>(null);  const [probe, setProbe] = useState<ProbeResult | null>(null);  const [oauthAuth, setOauthAuth] = useState<OAuthAuth | null>(null);  const [loadingProbe, setLoadingProbe] = useState(false);  const [startingOAuth, setStartingOAuth] = useState(false);  const [adding, setAdding] = useState(false);  const [error, setError] = useState<string | null>(null);  const [showScopes, setShowScopes] = useState(false);   const scopeId = useScope();  const doProbe = useAtomSet(probeGoogleDiscovery, { mode: "promise" });  const doAdd = useAtomSet(addGoogleDiscoverySource, { mode: "promise" });  const doStartOAuth = useAtomSet(startGoogleDiscoveryOAuth, { mode: "promise" });  const secrets = useAtomValue(secretsAtom(scopeId));   const canUseOAuth = useMemo(    () => (probe?.scopes.length ?? 0) > 0,    [probe],  );  const secretList: readonly SecretPickerSecret[] = Result.match(secrets, {    onInitial: () => [] as SecretPickerSecret[],    onFailure: () => [] as SecretPickerSecret[],    onSuccess: ({ value }) =>      value.map((secret) => ({        id: secret.id,        name: secret.name,        provider: secret.provider ? String(secret.provider) : undefined,      })),  });   const applyTemplate = useCallback((template: GoogleDiscoveryTemplate) => {    setSelectedTemplateId(template.id);    setDiscoveryUrl(template.discoveryUrl);    setName(template.name);    setClientSecretSecretId(null);    setProbe(null);    setOauthAuth(null);    setError(null);    setShowScopes(false);    setAuthKind("oauth2");  }, []);   const handleProbe = useCallback(async () => {    setLoadingProbe(true);    setError(null);    setOauthAuth(null);    setShowScopes(false);    try {      const result = await doProbe({        path: { scopeId },        payload: { discoveryUrl: discoveryUrl.trim() },      });      setProbe({ ...result, scopes: [...result.scopes] });      if (!name.trim()) {        setName(result.name);      }      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, name]); 

Lines 1–500 of 814.