import { connectionServicePresentation } from "./connection-service-presentation"; import { ConnectionCatalog } from "./connection-catalog"; import { ConnectionLogo } from "./connection-logo"; import { useEffect, useState, type FormEvent } from "react"; import type { ConnectAccount, ConnectController, ConnectProvider } from "@opengeni/connect"; import { useConnect } from "./hooks/use-connect"; import { useOpenGeniText } from "./messages/runtime"; export type ConnectChooserProps = { controller: ConnectController; /** Exact host return string; no completion parameters are added. */ returnUrl: string; className?: string; presentation?: "select" | "catalog"; /** Compact mechanism picker when service discovery is already shown by the host. */ customOnly?: boolean; providerOnly?: boolean; compact?: boolean; /** Select a service from the unified discovery list. */ initialProviderId?: string; }; /** Catalog readiness is supplied by the authenticated backend, never inferred * from the existence of a provider definition. Host replaces the controller on * actor/workspace change. This form does not open OAuth windows itself. */ export function ConnectChooser(props: ConnectChooserProps) { // Remount scope-local selections synchronously when the controller changes, // before effects can expose a previous actor's inventory for one frame. const [scope, setScope] = useState(props.controller); const [generation, setGeneration] = useState(0); if (scope !== props.controller) { setScope(props.controller); setGeneration(generation + 1); } return ; } function ScopedChooser({ controller, returnUrl, className, presentation = "select", customOnly = false, providerOnly = false, compact = false, initialProviderId = "", }: ConnectChooserProps) { const t = useOpenGeniText(); const view = useConnect(controller); const [catalog, setCatalog] = useState(null); const [accounts, setAccounts] = useState([]); const [failed, setFailed] = useState(false); const [load, setLoad] = useState(0); const [query, setQuery] = useState(""); const [providerId, setProviderId] = useState(initialProviderId); const [ownership, setOwnership] = useState(""); useEffect(() => { const abort = new AbortController(); setCatalog(null); setAccounts([]); setFailed(false); setProviderId(initialProviderId); setOwnership(""); // Promise boundary also handles a synchronously throwing injected transport. void Promise.resolve() .then(() => { abort.signal.throwIfAborted(); return Promise.all([ controller.transport.catalog(controller.workspaceId, { signal: abort.signal }), presentation === "catalog" ? controller.transport.accounts(controller.workspaceId, { signal: abort.signal }) : Promise.resolve([] as ConnectAccount[]), ]); }) .then(([providers, inventory]) => { if (!abort.signal.aborted) { setCatalog( structuredClone( providers.filter( (entry) => entry.readiness === "available" && entry.ownership.length > 0 && (!providerOnly || (entry.family !== "mcp" && !entry.setup.some((kind) => ["installation", "openapi", "graphql"].includes(kind), ))) && (!customOnly || entry.family === "mcp" || entry.setup.some((kind) => ["installation", "openapi", "graphql"].includes(kind), )), ), ), ); setAccounts(structuredClone(inventory)); const initial = providers.find((entry) => entry.id === initialProviderId); if (initial) setOwnership( initial.ownership.includes("personal") && ["google", "microsoft"].includes(initial.family) ? "personal" : initial.ownership.includes("workspace") ? "workspace" : (initial.ownership[0] ?? ""), ); } }) .catch(() => { if (!abort.signal.aborted) setFailed(true); }); return () => abort.abort(); }, [controller, load, presentation, customOnly, providerOnly, initialProviderId]); const provider = catalog?.find((entry) => entry.id === providerId); const canBegin = provider?.readiness === "available" && (ownership === "personal" || ownership === "workspace") && provider.ownership.includes(ownership); const submit = (event: FormEvent) => { event.preventDefault(); if (!canBegin || view.busy || (ownership !== "personal" && ownership !== "workspace")) return; setFailed(false); try { void controller .begin({ providerId, ownership, returnUrl, idempotencyKey: crypto.randomUUID() }) .catch(() => setFailed(true)); } catch { setFailed(true); } }; // Optional adapters must not add a temporary section that disappears once // readiness arrives. The primary discovery surface already owns loading. if (compact && (!catalog || catalog.length === 0) && !failed) return null; return ( {failed && {t("connectChooser.connectionsCouldNotBeLoaded")}} {!catalog && !failed && {t("connectChooser.loadingConnections")}} {catalog?.length === 0 && ( {t("connectChooser.noConnectionsAreAvailable")} )} {Boolean(catalog?.length) && ( {presentation === "catalog" ? t("connectChooser.addAConnection") : t("connectChooser.providerAndOwnership")} {presentation === "catalog" && !provider ? ( <> {t("connectChooser.searchConnections")} setQuery(event.target.value)} /> {[ { name: t("connectChooser.services"), entries: catalog!.filter( (entry) => entry.readiness === "available" && entry.family !== "mcp" && !entry.setup.some((kind) => ["installation", "openapi", "graphql"].includes(kind), ), ), secondary: false, }, { name: t("connectChooser.customConnections"), entries: catalog!.filter( (entry) => entry.readiness === "available" && (entry.family === "mcp" || entry.setup.some((kind) => ["installation", "openapi", "graphql"].includes(kind), )), ), secondary: true, }, ].map((group) => { const entries = group.entries.filter((entry) => entry.label.toLocaleLowerCase().includes(query.toLocaleLowerCase().trim()), ); if (!entries.length) return null; const rows = ( { const connected = accounts.some( (account) => account.providerId === entry.id && account.status === "connected", ); return { id: entry.id, name: entry.label, logo: ( ), options: [ { id: entry.id, name: entry.label, status: connected ? t("connectChooser.connected") : entry.readiness === "available" ? t("connectChooser.connect") : t("connectChooser.unavailable"), state: connected ? "added" : entry.readiness === "available" ? "available" : "unavailable", connected, onOpen: () => { if (!view.busy) { setProviderId(entry.id); setOwnership( entry.ownership.length === 1 ? entry.ownership[0]! : "", ); } }, }, ], }; })} /> ); return group.secondary && !customOnly ? ( {group.name} {entries.length} {rows} ) : ( {rows} ); })} {!catalog!.some((entry) => entry.label.toLocaleLowerCase().includes(query.toLocaleLowerCase().trim()), ) && {t("connectChooser.noServicesMatchYourSearch")}} > ) : null} {presentation === "catalog" && provider && !initialProviderId ? ( { setProviderId(""); setOwnership(""); }} > {t("connectChooser.backToConnections")} ) : null} {t("connectChooser.provider")} { setProviderId(event.target.value); setOwnership(""); }} > {t("connectChooser.chooseAProvider")} {catalog!.map((entry) => ( {entry.label} — {entry.id} ({entry.readiness.replaceAll("_", " ")}) ))} {provider && ( <> {presentation === "catalog" ? provider.label : null} {provider.readiness !== "available" && ( {provider.reason ?? t("connectChooser.thisServiceIsNotAvailable")} )} {t("connectChooser.whoCanUseThisConnection")} setOwnership(event.target.value)} > {t("connectChooser.chooseOwnership")} {provider.ownership.map((choice) => ( {choice === "personal" ? t("connectChooser.onlyMe") : t("connectChooser.everyoneInThisWorkspace")} ))} > )} {t("connectChooser.continue")} )} setLoad(load + 1)}> {t("connectChooser.reloadProviders")} ); }
{t("connectChooser.connectionsCouldNotBeLoaded")}
{t("connectChooser.loadingConnections")}
{t("connectChooser.noConnectionsAreAvailable")}
{t("connectChooser.noServicesMatchYourSearch")}
{provider.reason ?? t("connectChooser.thisServiceIsNotAvailable")}