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…</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. Load more
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…</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, );