import { useCallback, useMemo } from "react";import { useAtomValue, useAtomSet } from "@effect/atom-react";import * as Exit from "effect/Exit";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import { AuthTemplateSlug, IntegrationSlug } from "@executor-js/sdk/shared";import type { IntegrationAccountHandoff } from "@executor-js/sdk/client";import { TriangleAlert } from "lucide-react"; import { AccountsSection } from "@executor-js/react/components/accounts-section";import { HealthCheckEditor, type HealthCheckLivePreview,} from "@executor-js/react/components/health-check-editor";import { useOrganizationId } from "@executor-js/react/api/organization-context";import { defaultConnectionOwnerForHost } from "@executor-js/react/plugins/connection-owner";import { Alert, AlertDescription, AlertTitle } from "@executor-js/react/components/alert";import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";import type { AuthMethod, Placement } from "@executor-js/react/lib/auth-placements";import { useCustomMethodActions, type AuthMethodsCodec, type ConfigureAuthMethods,} from "@executor-js/react/lib/custom-auth-methods";import { authMethodsFromConfig, openApiWireAuthInput, templateFromPlacements,} from "@executor-js/plugin-openapi/react";import type { Authentication } from "@executor-js/plugin-openapi"; import { googleConfigAtom, googleConfigure } from "./atoms";import { googleAudienceWarningsForUrls } from "../sdk/presets"; const GOOGLE_AUDIENCE_WARNING: Readonly<Record<string, string>> = { "workspace-admin": "This connection includes Google Workspace admin APIs. Connecting requires a Workspace admin account; personal Gmail accounts cannot grant these scopes.", "unsupported-user": "This connection includes APIs that Google does not grant through standard user OAuth consent. Those tools may fail to authorize.",}; const NO_AUTH_METHOD: AuthMethod = { id: "none", label: "No authentication", kind: "none", source: "spec", template: AuthTemplateSlug.make("none"), placements: [],}; export default function GoogleAccountsPanel(props: { readonly sourceId: string; readonly integrationName: string; readonly accountHandoff?: IntegrationAccountHandoff | null;}) { const { sourceId, integrationName, accountHandoff } = props; const slug = IntegrationSlug.make(sourceId); const configResult = useAtomValue(googleConfigAtom(slug)); const doConfigure = useAtomSet(googleConfigure, { mode: "promiseExit" }); const existingTemplate = useMemo<readonly Authentication[]>(() => { if (!AsyncResult.isSuccess(configResult) || configResult.value == null) return []; return (configResult.value.authenticationTemplate ?? []) as readonly Authentication[]; }, [configResult]); const methods = useMemo<readonly AuthMethod[]>(() => { const declared = authMethodsFromConfig(existingTemplate); return declared.length > 0 ? declared : [NO_AUTH_METHOD]; }, [existingTemplate]); const configure = useCallback<ConfigureAuthMethods<Authentication>>( async (input) => { const exit = await doConfigure({ params: { slug }, payload: { authenticationTemplate: input.authenticationTemplate.map(openApiWireAuthInput), ...(input.mode ? { mode: input.mode } : {}), }, reactivityKeys: integrationWriteKeys, }); return Exit.map(exit, (result) => result.authenticationTemplate as readonly Authentication[]); }, [doConfigure, slug], ); const codec = useMemo<AuthMethodsCodec<Authentication>>( () => ({ toAuthMethods: authMethodsFromConfig, templatesFromPlacements: (placements: readonly Placement[]) => [ templateFromPlacements(placements), ], slugOf: (template: Authentication) => String(template.slug), }), [], ); const { createCustomMethod, removeCustomMethod } = useCustomMethodActions({ existing: existingTemplate, codec, configure, }); const audienceWarnings = useMemo<readonly string[]>(() => { if (!AsyncResult.isSuccess(configResult) || configResult.value == null) return []; const urls = configResult.value.googleDiscoveryUrls ?? []; return googleAudienceWarningsForUrls(urls).flatMap((audience: string) => { const message = GOOGLE_AUDIENCE_WARNING[audience]; return message ? [message] : []; }); }, [configResult]); // Live-preview context for the health-check edit sheet (apiKey methods only; // OAuth methods can't be previewed with a pasted key). const organizationId = useOrganizationId(); const livePreview = useMemo<HealthCheckLivePreview | undefined>(() => { const templates = methods .filter((m) => m.kind === "apikey") .map((m) => ({ template: m.template, label: m.label })); if (templates.length === 0) return undefined; return { owner: defaultConnectionOwnerForHost(organizationId), templates }; }, [methods, organizationId]); return ( <div className="mx-auto max-w-3xl space-y-8 px-6 py-8"> {audienceWarnings.length > 0 && ( <Alert variant="destructive"> <TriangleAlert /> <AlertTitle>Some Google APIs need special consent</AlertTitle> <AlertDescription> {audienceWarnings.map((message: string) => ( <p key={message}>{message}</p> ))} </AlertDescription> </Alert> )} <AccountsSection integration={slug} integrationName={integrationName} methods={methods} accountHandoff={accountHandoff} createCustomMethod={createCustomMethod} removeCustomMethod={removeCustomMethod} /> {/* Google auto-configures the People identity probe at add time; this is where an admin sees, adjusts, or clears it. */} <HealthCheckEditor integration={slug} livePreview={livePreview} /> </div> );}
import { useCallback, useMemo } from "react";
import { useAtomValue, useAtomSet } from "@effect/atom-react";
import * as Exit from "effect/Exit";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import { AuthTemplateSlug, IntegrationSlug } from "@executor-js/sdk/shared";
import type { IntegrationAccountHandoff } from "@executor-js/sdk/client";
import { TriangleAlert } from "lucide-react";
import { AccountsSection } from "@executor-js/react/components/accounts-section";
import {
HealthCheckEditor,
type HealthCheckLivePreview,
} from "@executor-js/react/components/health-check-editor";
import { useOrganizationId } from "@executor-js/react/api/organization-context";
import { defaultConnectionOwnerForHost } from "@executor-js/react/plugins/connection-owner";
import { Alert, AlertDescription, AlertTitle } from "@executor-js/react/components/alert";
import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
import type { AuthMethod, Placement } from "@executor-js/react/lib/auth-placements";
import {
useCustomMethodActions,
type AuthMethodsCodec,
type ConfigureAuthMethods,
} from "@executor-js/react/lib/custom-auth-methods";
import {
authMethodsFromConfig,
openApiWireAuthInput,
templateFromPlacements,
} from "@executor-js/plugin-openapi/react";
import type { Authentication } from "@executor-js/plugin-openapi";
import { googleConfigAtom, googleConfigure } from "./atoms";
import { googleAudienceWarningsForUrls } from "../sdk/presets";
const GOOGLE_AUDIENCE_WARNING: Readonly<Record<string, string>> = {
"workspace-admin":
"This connection includes Google Workspace admin APIs. Connecting requires a Workspace admin account; personal Gmail accounts cannot grant these scopes.",
"unsupported-user":
"This connection includes APIs that Google does not grant through standard user OAuth consent. Those tools may fail to authorize.",
};
const NO_AUTH_METHOD: AuthMethod = {
id: "none",
label: "No authentication",
kind: "none",
source: "spec",
template: AuthTemplateSlug.make("none"),
placements: [],
};
export default function GoogleAccountsPanel(props: {
readonly sourceId: string;
readonly integrationName: string;
readonly accountHandoff?: IntegrationAccountHandoff | null;
}) {
const { sourceId, integrationName, accountHandoff } = props;
const slug = IntegrationSlug.make(sourceId);
const configResult = useAtomValue(googleConfigAtom(slug));
const doConfigure = useAtomSet(googleConfigure, { mode: "promiseExit" });
const existingTemplate = useMemo<readonly Authentication[]>(() => {
if (!AsyncResult.isSuccess(configResult) || configResult.value == null) return [];
return (configResult.value.authenticationTemplate ?? []) as readonly Authentication[];
}, [configResult]);
const methods = useMemo<readonly AuthMethod[]>(() => {
const declared = authMethodsFromConfig(existingTemplate);
return declared.length > 0 ? declared : [NO_AUTH_METHOD];
}, [existingTemplate]);
const configure = useCallback<ConfigureAuthMethods<Authentication>>(
async (input) => {
const exit = await doConfigure({
params: { slug },
payload: {
authenticationTemplate: input.authenticationTemplate.map(openApiWireAuthInput),
...(input.mode ? { mode: input.mode } : {}),
},
reactivityKeys: integrationWriteKeys,
});
return Exit.map(exit, (result) => result.authenticationTemplate as readonly Authentication[]);
},
[doConfigure, slug],
);
const codec = useMemo<AuthMethodsCodec<Authentication>>(
() => ({
toAuthMethods: authMethodsFromConfig,
templatesFromPlacements: (placements: readonly Placement[]) => [
templateFromPlacements(placements),
],
slugOf: (template: Authentication) => String(template.slug),
}),
[],
);
const { createCustomMethod, removeCustomMethod } = useCustomMethodActions({
existing: existingTemplate,
codec,
configure,
});
const audienceWarnings = useMemo<readonly string[]>(() => {
if (!AsyncResult.isSuccess(configResult) || configResult.value == null) return [];
const urls = configResult.value.googleDiscoveryUrls ?? [];
return googleAudienceWarningsForUrls(urls).flatMap((audience: string) => {
const message = GOOGLE_AUDIENCE_WARNING[audience];
return message ? [message] : [];
});
}, [configResult]);
// Live-preview context for the health-check edit sheet (apiKey methods only;
// OAuth methods can't be previewed with a pasted key).
const organizationId = useOrganizationId();
const livePreview = useMemo<HealthCheckLivePreview | undefined>(() => {
const templates = methods
.filter((m) => m.kind === "apikey")
.map((m) => ({ template: m.template, label: m.label }));
if (templates.length === 0) return undefined;
return { owner: defaultConnectionOwnerForHost(organizationId), templates };
}, [methods, organizationId]);
return (
<div className="mx-auto max-w-3xl space-y-8 px-6 py-8">
{audienceWarnings.length > 0 && (
<Alert variant="destructive">
<TriangleAlert />
<AlertTitle>Some Google APIs need special consent</AlertTitle>
<AlertDescription>
{audienceWarnings.map((message: string) => (
<p key={message}>{message}</p>
))}
</AlertDescription>
</Alert>
)}
<AccountsSection
integration={slug}
integrationName={integrationName}
methods={methods}
accountHandoff={accountHandoff}
createCustomMethod={createCustomMethod}
removeCustomMethod={removeCustomMethod}
/>
{/* Google auto-configures the People identity probe at add time; this is
where an admin sees, adjusts, or clears it. */}
<HealthCheckEditor integration={slug} livePreview={livePreview} />
</div>
);
}