681 lines · 24.8 KB · TypeScript
RawDownload
import { Suspense, useCallback, useMemo, useState, type ReactNode } from "react";import { Link, useNavigate } from "@tanstack/react-router";import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import { PlusIcon } from "lucide-react";import type { Integration, IntegrationDetectionResult } from "@executor-js/sdk/shared";import {  useIntegrationPlugins,  type IntegrationPlugin,  type IntegrationPreset,} from "@executor-js/sdk/client";import { detectIntegration, integrationsOptimisticAtom } from "../api/atoms";import { trackEvent } from "../api/analytics";import { McpInstallCard } from "../components/mcp-install-card";import { Button } from "../components/button";import { PageContainer, PageHeader } from "../components/page";import { Badge } from "../components/badge";import { Input } from "../components/input";import {  Dialog,  DialogContent,  DialogDescription,  DialogHeader,  DialogTitle,} from "../components/dialog";import {  CardStack,  CardStackContent,  CardStackEntry,  CardStackEntryActions,  CardStackEntryContent,  CardStackEntryDescription,  CardStackEntryMedia,  CardStackEntryTitle,  CardStackHeader,} from "../components/card-stack";import {  IntegrationFavicon,  integrationInferredUrl,  integrationPresetIconUrl,} from "../components/integration-favicon";import { groupIntegrations, type IntegrationFamilyGroup } from "../lib/integration-grouping";import {  availableCatalogKinds,  catalogLogoUrl,  filterCatalogEntries,  presetDomains,  resolveConnectTarget,  useCatalogSearch,  type CatalogKind,  type CatalogSearchEntry,} from "../lib/integrations-sh-catalog";import { IntegrationHealthSummary } from "../components/integration-health-summary";import { IntegrationIconWithAccount } from "../components/integration-icon-with-account";import { Skeleton } from "../components/skeleton";import { useExecutorDocumentTitle } from "../lib/document-title";import { ErrorState } from "../components/error-state";import { isAsyncResultLoading } from "../lib/async-result"; const KIND_TO_PLUGIN_KEY: Record<string, string> = {  openapi: "openapi",  mcp: "mcp",  graphql: "graphql",  googleDiscovery: "google",}; const detectionRank: Record<IntegrationDetectionResult["confidence"], number> = {  high: 3,  medium: 2,  low: 1,}; const bestDetection = (  results: readonly IntegrationDetectionResult[],): IntegrationDetectionResult | undefined =>  [...results].sort((a, b) => detectionRank[b.confidence] - detectionRank[a.confidence])[0]; // ---------------------------------------------------------------------------// Page// --------------------------------------------------------------------------- export function IntegrationsPage() {  useExecutorDocumentTitle("Integrations");  const integrations = useAtomValue(integrationsOptimisticAtom);  const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom);  const [connectOpen, setConnectOpen] = useState(false);   return (    <PageContainer>      <PageHeader        title="Integrations"        description="Tool providers available in this workspace."        actions={          <Button            onClick={() => {              setConnectOpen(true);              trackEvent("integration_connect_dialog_opened");            }}            size="sm"            className="gap-1.5"          >            <PlusIcon className="size-4" />            Connect          </Button>        }      />       <div className="mb-8">        <McpInstallCard />      </div>       <div className="mb-8 border-t border-border/50" />       {isAsyncResultLoading(integrations) ? (        <IntegrationsGridSkeleton />      ) : (        AsyncResult.match(integrations, {          onInitial: () => <IntegrationsGridSkeleton />,          onFailure: () => (            <ErrorState message="Failed to load integrations" onRetry={refreshIntegrations} />          ),          onSuccess: ({ value }) => {            if (value.length === 0) {              return (                <EmptyIntegrations                  onConnect={() => {                    setConnectOpen(true);                    trackEvent("integration_connect_dialog_opened");                  }}                />              );            }             return (              <div className="mb-8 space-y-3">                <IntegrationGrid integrations={value} />              </div>            );          },        })      )}       <ConnectDialog open={connectOpen} onOpenChange={setConnectOpen} />    </PageContainer>  );} // ---------------------------------------------------------------------------// Connect dialog — URL detection + manual plugin chooser + presets// --------------------------------------------------------------------------- // Heuristic: the input either looks like a URL (auto-detect) or a free-text// search query (filter the preset list). Anything with a scheme, slash, or// host-with-TLD is treated as a URL; everything else is search.const looksLikeUrl = (raw: string): boolean => {  const v = raw.trim();  if (v.length === 0) return false;  if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(v)) return true;  if (v.includes("/")) return true;  if (/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}(?::\d+)?$/i.test(v)) return true;  return false;}; export function ConnectDialog(props: { open: boolean; onOpenChange: (open: boolean) => void }) {  const integrationPlugins = useIntegrationPlugins();  const doDetect = useAtomSet(detectIntegration, { mode: "promiseExit" });  const navigate = useNavigate();   const [query, setQuery] = useState("");  const [detecting, setDetecting] = useState(false);  const [error, setError] = useState<string | null>(null);   const isUrl = looksLikeUrl(query);  const presetSearch = isUrl ? "" : query;   const closeAndReset = useCallback(() => {    setQuery("");    setError(null);    setDetecting(false);    props.onOpenChange(false);  }, [props]);   const handleDetect = useCallback(async () => {    const trimmed = query.trim();    if (!trimmed) return;    setDetecting(true);    setError(null);    // Detection is read-only — it inspects a URL and returns candidates without    // mutating the catalog, so it invalidates nothing.    const exit = await doDetect({      payload: { url: trimmed },      reactivityKeys: [],    });    if (Exit.isFailure(exit)) {      trackEvent("integration_detect_submitted", { success: false });      setError("Detection failed. Try adding an integration manually.");      setDetecting(false);      return;    }    const results = exit.value;    if (results.length === 0) {      trackEvent("integration_detect_submitted", { success: false });      setError("Could not detect an integration type from this URL. Try adding manually.");      setDetecting(false);      return;    }    const detected = bestDetection(results);    if (!detected) {      trackEvent("integration_detect_submitted", { success: false });      setError("Could not detect an integration type from this URL. Try adding manually.");      setDetecting(false);      return;    }    trackEvent("integration_detect_submitted", {      success: true,      detected_kind: detected.kind,      confidence: detected.confidence,    });    const pluginKey = KIND_TO_PLUGIN_KEY[detected.kind] ?? detected.kind;    if (integrationPlugins.some((p) => p.key === pluginKey)) {      trackEvent("integration_add_started", { plugin_key: pluginKey, via: "detect" });      closeAndReset();      void navigate({        to: "/{-$orgSlug}/integrations/add/$pluginKey",        params: { pluginKey },        search: { url: trimmed, namespace: detected.slug },      });    } else {      setError(`Detected integration type "${detected.kind}" but no plugin is available for it.`);      setDetecting(false);    }  }, [query, doDetect, navigate, integrationPlugins, closeAndReset]);   return (    <Dialog      open={props.open}      onOpenChange={(open) => {        if (!open) closeAndReset();        else props.onOpenChange(open);      }}    >      <DialogContent className="sm:max-w-[560px]">        <DialogHeader>          <DialogTitle>Connect an integration</DialogTitle>          <DialogDescription>            Search the preset library, or paste a URL to auto-detect.          </DialogDescription>        </DialogHeader>         <div className="flex min-w-0 flex-col gap-5">          <div className="flex flex-col gap-2">            <div className="flex gap-2">              <Input                type="text"                value={query}                onChange={(e) => {                  setQuery((e.target as HTMLInputElement).value);                  setError(null);                }}                onKeyDown={(e) => {                  if (e.key === "Enter" && isUrl) void handleDetect();                }}                placeholder="Search or paste a URL…"                disabled={detecting}                className="flex-1"              />              {isUrl && (                <Button onClick={() => void handleDetect()} disabled={detecting || !query.trim()}>                  {detecting ? "Detecting..." : "Detect"}                </Button>              )}            </div>            {error && <p className="text-xs text-destructive">{error}</p>}          </div>           <div className="flex flex-col gap-2">            <p className="text-xs font-medium text-foreground/80">Or add manually</p>            <div className="flex flex-wrap gap-2">              {integrationPlugins.map((p) => (                <Link                  key={p.key}                  to="/{-$orgSlug}/integrations/add/$pluginKey"                  params={{ pluginKey: p.key }}                  onClick={() => {                    trackEvent("integration_add_started", { plugin_key: p.key, via: "manual" });                    closeAndReset();                  }}                  className="rounded-md border border-border px-3 py-1.5 text-xs font-medium transition-colors hover:bg-muted"                >                  {p.label}                </Link>              ))}            </div>          </div>           <PresetGrid            plugins={integrationPlugins}            onPick={closeAndReset}            searchQuery={presetSearch}          />        </div>      </DialogContent>    </Dialog>  );} // ---------------------------------------------------------------------------// Empty state// --------------------------------------------------------------------------- function EmptyIntegrations(props: { onConnect: () => void }) {  return (    <div className="mb-8 flex flex-col items-center justify-center rounded-2xl border border-dashed border-border py-16">      <div className="mb-4 flex size-12 items-center justify-center rounded-2xl bg-muted text-muted-foreground">        <PlusIcon className="size-5" />      </div>      <p className="mb-1 text-[14px] font-medium text-foreground/70">No integrations yet</p>      <p className="mb-5 text-[13px] text-muted-foreground/60">        Connect an integration to start curating tools.      </p>      <Button onClick={props.onConnect} size="sm" className="gap-1.5">        <PlusIcon className="size-4" />        Connect an integration      </Button>    </div>  );} // ---------------------------------------------------------------------------// Preset grid (for inside the Connect dialog)// --------------------------------------------------------------------------- type PresetEntry = {  preset: IntegrationPreset;  pluginKey: string;  pluginLabel: string;}; const CATALOG_KIND_LABEL: Record<CatalogKind, string> = {  mcp: "MCP",  openapi: "OpenAPI",  graphql: "GraphQL",}; function PresetGrid(props: {  plugins: readonly IntegrationPlugin[];  onPick: () => void;  /** Controlled filter query forwarded from the dialog's unified   *  search/URL input. Empty string disables filtering. */  searchQuery?: string;}) {  const navigate = useNavigate();  const allPresets = useMemo(() => {    const entries: PresetEntry[] = [];    for (const plugin of props.plugins) {      for (const preset of plugin.presets ?? []) {        entries.push({          preset,          pluginKey: plugin.key,          pluginLabel: plugin.label,        });      }    }    return entries;  }, [props.plugins]);   const query = (props.searchQuery ?? "").trim();   const filtered = useMemo(() => {    const q = query.toLowerCase();    if (q.length === 0) return allPresets;    return allPresets.filter(({ preset, pluginLabel }) => {      const corpus =        `${preset.name} ${preset.summary ?? ""} ${preset.family ?? ""} ${preset.specFormat ?? ""} ${pluginLabel}`.toLowerCase();      return corpus.includes(q);    });  }, [allPresets, query]);   // Long-tail search over the public integrations.sh registry, shown under the  // curated presets. Domains a preset already covers stay preset-only.  const catalog = useCatalogSearch(query);  const excludeDomains = useMemo(() => presetDomains(props.plugins), [props.plugins]);  const availableKinds = useMemo(() => availableCatalogKinds(props.plugins), [props.plugins]);  const catalogEntries = useMemo(    () => filterCatalogEntries(catalog.entries, { excludeDomains, availableKinds }),    [catalog.entries, excludeDomains, availableKinds],  );  const [resolvingDomain, setResolvingDomain] = useState<string | null>(null);  const [catalogError, setCatalogError] = useState<string | null>(null);   const pickCatalogEntry = useCallback(    async (entry: CatalogSearchEntry) => {      if (resolvingDomain !== null) return;      setResolvingDomain(entry.domain);      setCatalogError(null);      const exit = await Effect.runPromiseExit(resolveConnectTarget(entry.domain, entry.kinds));      setResolvingDomain(null);      if (Exit.isFailure(exit)) {        setCatalogError(          `Couldn't load connect details for ${entry.domain}. Paste a URL above instead.`,        );        return;      }      const target = exit.value;      if (!target) {        setCatalogError(          `No connectable endpoint is on record for ${entry.domain}. Paste a URL above to detect one.`,        );        return;      }      trackEvent("integration_add_started", {        plugin_key: target.kind,        via: "catalog",        catalog_domain: entry.domain,      });      props.onPick();      void navigate({        to: "/{-$orgSlug}/integrations/add/$pluginKey",        params: { pluginKey: target.kind },        search: { url: target.url, ...(target.slug ? { namespace: target.slug } : {}) },      });    },    [navigate, props, resolvingDomain],  );   const showCatalogSection = query.length > 0 && (catalogEntries.length > 0 || catalog.loading);   if (allPresets.length === 0) return null;   return (    <div className="flex min-w-0 flex-col gap-2">      <p className="text-xs font-medium text-foreground/80">Popular integrations</p>      <CardStack className="min-w-0">        {/* Fixed height keeps the dialog stable as the user filters; the         *  inner area scrolls when the list overflows and shows an empty         *  state when no presets match. */}        <CardStackContent className="h-64 overflow-y-auto">          {filtered.length === 0 && !showCatalogSection ? (            <div className="flex h-full flex-col items-center justify-center gap-1 px-4 py-6 text-center">              <p className="text-sm text-muted-foreground">No matching integrations</p>              <p className="text-xs text-muted-foreground/70">                Paste a URL above to auto-detect, or pick an integration type manually.              </p>            </div>          ) : (            filtered.map(({ preset, pluginKey, pluginLabel }) => {              const search: Record<string, string> = { preset: preset.id };              if (preset.url) search.url = preset.url;              return (                <CardStackEntry key={`${pluginKey}-${preset.id}`} asChild>                  <Link                    to="/{-$orgSlug}/integrations/add/$pluginKey"                    params={{ pluginKey }}                    search={search}                    onClick={() => {                      trackEvent("integration_add_started", {                        plugin_key: pluginKey,                        via: "preset",                        preset_id: preset.id,                      });                      props.onPick();                    }}                  >                    <CardStackEntryMedia>                      {preset.icon ? (                        <img                          src={preset.icon}                          alt=""                          className="size-5 object-contain"                          loading="lazy"                        />                      ) : (                        <svg viewBox="0 0 16 16" className="size-3.5" fill="none">                          <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.2" />                        </svg>                      )}                    </CardStackEntryMedia>                    <CardStackEntryContent>                      <CardStackEntryTitle>{preset.name}</CardStackEntryTitle>                      <CardStackEntryDescription>{preset.summary}</CardStackEntryDescription>                    </CardStackEntryContent>                    <CardStackEntryActions>                      <Badge variant="secondary">{pluginLabel}</Badge>                    </CardStackEntryActions>                  </Link>                </CardStackEntry>              );            })          )}          {showCatalogSection && (            <>              {catalogEntries.map((entry) => (                <CardStackEntry key={`catalog-${entry.domain}`} asChild>                  <Button                    type="button"                    variant="ghost"                    onClick={() => void pickCatalogEntry(entry)}                    className="h-auto w-full justify-start rounded-none text-left font-normal whitespace-normal hover:bg-accent/40 dark:hover:bg-accent/40"

Lines 1–500 of 681.