1364 lines · 42.8 KB · TypeScript
RawDownload
import { startTransition, useMemo, useState, type ReactNode } from "react";import type { Source } from "@executor/react";import {  SecretReferenceField,  defineExecutorPluginHttpApiClient,  Result,  useAtomSet,  useAtomValue,  useExecutorMutation,  useLocalInstallation,  useSource,} from "@executor/react";import {  Alert,  Badge,  Button,  IconPencil,  Input,  Label,  Select,  Separator,  SourceToolExplorer,  Textarea,  parseSourceToolExplorerSearch,  type SourceToolExplorerSearch,  useSourcePluginNavigation,  useSourcePluginRouteParams,  useSourcePluginSearch,} from "@executor/react/plugins"; import {  googleDiscoveryHttpApiExtension,} from "@executor/plugin-google-discovery-http";import {  GOOGLE_DISCOVERY_OAUTH_CALLBACK_PATH,  GOOGLE_DISCOVERY_OAUTH_STORAGE_PREFIX,  GOOGLE_DISCOVERY_PLUGIN_KEY,  GOOGLE_DISCOVERY_SOURCE_KIND,  defaultGoogleDiscoveryUrl,  type GoogleDiscoveryBatchSourceInput,  type GoogleDiscoveryConnectInput,  type GoogleDiscoveryConnectionAuth,  type GoogleDiscoveryOAuthPopupResult,  type GoogleDiscoveryStartBatchOAuthInput,  type GoogleDiscoveryStartOAuthInput,} from "@executor/plugin-google-discovery-shared";import { getGoogleDiscoveryIconUrlForService } from "./icons"; const OAUTH_TIMEOUT_MS = 2 * 60_000; const getGoogleDiscoveryHttpClient =  defineExecutorPluginHttpApiClient<"GoogleDiscoveryReactHttpClient">()(    "GoogleDiscoveryReactHttpClient",    [googleDiscoveryHttpApiExtension] as const,  ); const defaultGoogleInput = (): GoogleDiscoveryConnectInput => ({  name: "Google Sheets",  service: "sheets",  version: "v4",  discoveryUrl: defaultGoogleDiscoveryUrl("sheets", "v4"),  defaultHeaders: null,  scopes: [],  auth: {    kind: "none",  },}); type GoogleDiscoveryBatchTemplate = {  id: string;  name: string;  summary: string;  service: string;  version: string;  discoveryUrl: string;}; const googleDiscoveryTemplate = (  input: GoogleDiscoveryBatchTemplate,): GoogleDiscoveryBatchTemplate => ({  ...input,  discoveryUrl: input.discoveryUrl || defaultGoogleDiscoveryUrl(input.service, input.version),}); const GOOGLE_DISCOVERY_BATCH_TEMPLATES: ReadonlyArray<GoogleDiscoveryBatchTemplate> = [  googleDiscoveryTemplate({    id: "google-calendar",    name: "Google Calendar",    summary: "Calendars, events, ACLs, and scheduling workflows.",    service: "calendar",    version: "v3",    discoveryUrl: "https://calendar-json.googleapis.com/$discovery/rest?version=v3",  }),  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://gmail.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-docs",    name: "Google Docs",    summary: "Documents, structural edits, text ranges, and formatting.",    service: "docs",    version: "v1",    discoveryUrl: "https://docs.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-sheets",    name: "Google Sheets",    summary: "Spreadsheets, values, ranges, formatting, and batch updates.",    service: "sheets",    version: "v4",    discoveryUrl: "https://sheets.googleapis.com/$discovery/rest?version=v4",  }),  googleDiscoveryTemplate({    id: "google-slides",    name: "Google Slides",    summary: "Presentations, slides, page elements, and deck updates.",    service: "slides",    version: "v1",    discoveryUrl: "https://slides.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-forms",    name: "Google Forms",    summary: "Forms, questions, responses, quizzes, and form metadata.",    service: "forms",    version: "v1",    discoveryUrl: "https://forms.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-search-console",    name: "Google Search Console",    summary: "Sites, sitemaps, URL inspection, and search performance.",    service: "searchconsole",    version: "v1",    discoveryUrl: "https://searchconsole.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-people",    name: "Google People",    summary: "Contacts, profiles, directory people, and contact groups.",    service: "people",    version: "v1",    discoveryUrl: "https://people.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-tasks",    name: "Google Tasks",    summary: "Task lists, task items, notes, and due dates.",    service: "tasks",    version: "v1",    discoveryUrl: "https://tasks.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-chat",    name: "Google Chat",    summary: "Spaces, messages, members, reactions, and chat workflows.",    service: "chat",    version: "v1",    discoveryUrl: "https://chat.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-keep",    name: "Google Keep",    summary: "Notes, lists, attachments, and collaborative annotations.",    service: "keep",    version: "v1",    discoveryUrl: "https://keep.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-classroom",    name: "Google Classroom",    summary: "Courses, rosters, coursework, submissions, and grading data.",    service: "classroom",    version: "v1",    discoveryUrl: "https://classroom.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-admin-directory",    name: "Google Admin Directory",    summary: "Users, groups, org units, roles, and directory resources.",    service: "admin",    version: "directory_v1",    discoveryUrl: "https://admin.googleapis.com/$discovery/rest?version=directory_v1",  }),  googleDiscoveryTemplate({    id: "google-admin-reports",    name: "Google Admin Reports",    summary: "Audit events, usage reports, and admin activity logs.",    service: "admin",    version: "reports_v1",    discoveryUrl: "https://admin.googleapis.com/$discovery/rest?version=reports_v1",  }),  googleDiscoveryTemplate({    id: "google-apps-script",    name: "Google Apps Script",    summary: "Projects, deployments, execution, and script metadata.",    service: "script",    version: "v1",    discoveryUrl: "https://script.googleapis.com/$discovery/rest?version=v1",  }),  googleDiscoveryTemplate({    id: "google-bigquery",    name: "Google BigQuery",    summary: "Datasets, tables, jobs, routines, and analytics workflows.",    service: "bigquery",    version: "v2",    discoveryUrl: "https://bigquery.googleapis.com/$discovery/rest?version=v2",  }),  googleDiscoveryTemplate({    id: "google-cloud-resource-manager",    name: "Google Cloud Resource Manager",    summary: "Projects, folders, organizations, and IAM hierarchy.",    service: "cloudresourcemanager",    version: "v3",    discoveryUrl: "https://cloudresourcemanager.googleapis.com/$discovery/rest?version=v3",  }),  googleDiscoveryTemplate({    id: "google-youtube-data",    name: "YouTube Data",    summary: "Channels, playlists, videos, comments, captions, and uploads.",    service: "youtube",    version: "v3",    discoveryUrl: "https://youtube.googleapis.com/$discovery/rest?version=v3",  }),]; 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 parsedGoogleDiscoveryInputFromUrl = (  value: string | null,): Pick<GoogleDiscoveryConnectInput, "name" | "service" | "version" | "discoveryUrl"> | null => {  if (!value) {    return null;  }   try {    const url = new URL(value);    const byDirectory = url.pathname.match(/^\/discovery\/v1\/apis\/([^/]+)\/([^/]+)\/rest$/);    if (byDirectory) {      const service = decodeURIComponent(byDirectory[1] ?? "");      const version = decodeURIComponent(byDirectory[2] ?? "");      const knownTemplate = GOOGLE_DISCOVERY_BATCH_TEMPLATES.find(        (entry) => entry.service === service && entry.version === version,      );      return {        name: knownTemplate?.name ?? `Google ${service}`,        service,        version,        discoveryUrl: url.toString(),      };    }     const version = url.searchParams.get("version")?.trim() ?? "";    const isHostScopedDiscovery =      url.pathname === "/$discovery/rest"      && url.hostname.endsWith(".googleapis.com")      && url.hostname !== "www.googleapis.com";    if (version && isHostScopedDiscovery) {      const service = url.hostname.split(".")[0] ?? "";      const knownTemplate = GOOGLE_DISCOVERY_BATCH_TEMPLATES.find(        (entry) => entry.service === service && entry.version === version,      );      return {        name: knownTemplate?.name ?? `Google ${service}`,        service,        version,        discoveryUrl: url.toString(),      };    }  } catch {}   return null;}; const googleInputFromSearch = (  search: Record<string, unknown>,): GoogleDiscoveryConnectInput => {  const defaults = defaultGoogleInput();  const parsedFromInputUrl = parsedGoogleDiscoveryInputFromUrl(    presetString(search, "inputUrl")    ?? presetString(search, "input")    ?? presetString(search, "url"),  );  const service =    presetString(search, "presetService")    ?? parsedFromInputUrl?.service    ?? defaults.service;  const version =    presetString(search, "presetVersion")    ?? parsedFromInputUrl?.version    ?? defaults.version;   return {    ...defaults,    name:      presetString(search, "presetName")      ?? parsedFromInputUrl?.name      ?? defaults.name,    service,    version,    discoveryUrl:      presetString(search, "presetDiscoveryUrl")      ?? parsedFromInputUrl?.discoveryUrl      ?? defaultGoogleDiscoveryUrl(service, version),  };}; const stringifyStringMap = (  value: Record<string, string> | null | undefined,): string =>  !value || Object.keys(value).length === 0    ? ""    : JSON.stringify(value, null, 2); 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 stringifyScopes = (value: ReadonlyArray<string>): string =>  value.join("\n"); const parseScopes = (value: string): Array<string> =>  value    .split(/[\n,]/)    .map((entry) => entry.trim())    .filter(Boolean); const authSecretValue = (  input: GoogleDiscoveryConnectionAuth,): string =>  input.kind === "bearer"    ? JSON.stringify(input.tokenSecretRef)    : ""; const clientSecretValue = (  input: GoogleDiscoveryConnectionAuth,): string =>  input.kind === "oauth2" && input.clientSecretRef    ? JSON.stringify(input.clientSecretRef)    : ""; const waitForOauthPopupResult = async (  sessionId: string,): Promise<GoogleDiscoveryOAuthPopupResult> =>  new Promise((resolve, reject) => {    const storageKey = `${GOOGLE_DISCOVERY_OAUTH_STORAGE_PREFIX}${sessionId}`;    const startedAt = Date.now();     const cleanup = () => {      window.removeEventListener("message", handleMessage);      window.clearInterval(intervalId);    };     const finish = (result: GoogleDiscoveryOAuthPopupResult) => {      cleanup();      try {        window.localStorage.removeItem(storageKey);      } catch {}      resolve(result);    };     const handleMessage = (event: MessageEvent) => {      if (event.origin !== window.location.origin) {        return;      }       const data = event.data as GoogleDiscoveryOAuthPopupResult | undefined;      if (!data || data.type !== "executor:oauth-result") {        return;      }       if (data.ok && data.sessionId !== sessionId) {        return;      }       finish(data);    };     window.addEventListener("message", handleMessage);    const intervalId = window.setInterval(() => {      if (Date.now() - startedAt > OAUTH_TIMEOUT_MS) {        cleanup();        reject(new Error("Timed out waiting for Google OAuth to finish."));        return;      }       try {        const raw = window.localStorage.getItem(storageKey);        if (!raw) {          return;        }         finish(JSON.parse(raw) as GoogleDiscoveryOAuthPopupResult);      } catch {        // Ignore malformed local storage and continue polling.      }    }, 400);  }); const openGoogleOauthPopup = (name: string): Window | null => {  const popup = window.open("", name, "width=560,height=760");  if (!popup) {    return null;  }   try {    popup.document.title = "Sign in with Google";    popup.document.body.innerHTML = `      <main style="font-family: system-ui, -apple-system, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; color: #333;">        <div style="width: 20px; height: 20px; border: 2px solid #e5e7eb; border-top-color: #6366f1; border-radius: 50%; animation: spin 0.7s linear infinite; margin-bottom: 16px;"></div>        <p style="margin: 0; font-size: 14px; color: #888;">Redirecting to Google&hellip;</p>        <style>          @keyframes spin { to { transform: rotate(360deg); } }          @media (prefers-color-scheme: dark) {            main { background: #09090b; color: #fafafa !important; }            p { color: #a1a1aa !important; }            div { border-color: #3f3f46 !important; }          }        </style>      </main>    `;  } catch {    // Ignore browsers that don't allow writing to the popup document here.  }   return popup;}; const batchSourceInputFromTemplate = (  template: GoogleDiscoveryBatchTemplate,): GoogleDiscoveryBatchSourceInput => ({  name: template.name,  service: template.service,  version: template.version,  discoveryUrl: template.discoveryUrl,  defaultHeaders: null,  scopes: [],}); function GoogleDiscoverySourceForm(props: {  initialValue: GoogleDiscoveryConnectInput;  mode: "create" | "edit";  onSubmit: (input: GoogleDiscoveryConnectInput) => Promise<void>;}) {  const installation = useLocalInstallation();  const client = getGoogleDiscoveryHttpClient();  const startOAuth = useAtomSet(    client.mutation(GOOGLE_DISCOVERY_PLUGIN_KEY, "startOAuth"),    { mode: "promise" },  );  const submitMutation = useExecutorMutation<GoogleDiscoveryConnectInput, void>(props.onSubmit);  const [name, setName] = useState(props.initialValue.name);  const [service, setService] = useState(props.initialValue.service);  const [version, setVersion] = useState(props.initialValue.version);  const [discoveryUrl, setDiscoveryUrl] = useState(props.initialValue.discoveryUrl ?? "");  const [headersText, setHeadersText] = useState(    stringifyStringMap(props.initialValue.defaultHeaders),  );  const [scopesText, setScopesText] = useState(    stringifyScopes(props.initialValue.scopes),  );  const [authKind, setAuthKind] = useState<GoogleDiscoveryConnectionAuth["kind"]>(    props.initialValue.auth.kind,  );

Lines 1–500 of 1364.