674 lines · 26.2 KB · TypeScript
RawDownload
import { useEffect, useMemo, useState } from "react";import { useAtomValue, useAtomSet } from "@effect/atom-react";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import * as Exit from "effect/Exit";import { IntegrationSlug, type Connection, type Owner } from "@executor-js/sdk/shared";import type { IntegrationAccountHandoff } from "@executor-js/sdk/client";import { toast } from "sonner"; import {  addConnectionOptimistic,  integrationsAtom,  connectionsForIntegrationAtom,  providerAccountsAtom,  refreshConnection,  removeConnectionOptimistic,  startOAuth,} from "../api/atoms";import { connectionWriteKeys } from "../api/reactivity-keys";import { HEALTH_INDICATOR_COLOR, HEALTH_STATUS_LABEL } from "../lib/health-display";import { useConnectionHealth } from "../lib/use-connection-health";import { messageFromExit } from "../api/error-reporting";import { ownerLabel, useOwnerDisplay } from "../api/owner-display";import { trackEvent } from "../api/analytics";import type { AuthMethod } from "../lib/auth-placements";import {  MULTI_SERVICE_FAMILIES,  normalizeEmail,  type ProviderAccount,} from "../lib/provider-accounts";import {  connectionNeedsReconsent,  oauthReconnectPayload,  reconnectMode,  reconsentRequiredScopes,} from "../plugins/oauth-reconnect";import { useOAuthPopupFlow } from "../plugins/oauth-sign-in";import { AddAccountModal } from "./add-account-modal";import { EnableServicesModal, type EnableServiceIntegration } from "./enable-services-modal";import { ConnectionEditSheet } from "./metadata-edit-sheet";import type { CreateCustomMethod } from "./add-custom-method-modal";import { Badge } from "./badge";import { Button } from "./button";import {  CardStack,  CardStackContent,  CardStackEntry,  CardStackEntryActions,  CardStackEntryContent,  CardStackEntryDescription,  CardStackEntryTitle,  CardStackHeader,} from "./card-stack";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuItem,  DropdownMenuTrigger,} from "./dropdown-menu";import { PlusIcon } from "lucide-react"; // ---------------------------------------------------------------------------// Accounts section — the integration's connections, grouped by owner.//// Credentials are IMMUTABLE (no method switch); the editable surface is the// user-curated metadata — description (agent-visible) and account label — via// the per-row Edit sheet. "+ Add connection" opens the create modal. When both// owners have zero accounts, the section collapses to a single empty CTA.// --------------------------------------------------------------------------- const OWNERS: readonly Owner[] = ["org", "user"]; function AccountRow(props: {  readonly connection: Connection;  /** The integration declares scopes this connection was not granted — it must   *  reconnect to grant the newly-needed access (e.g. after a service was added). */  readonly needsReconsent: boolean;  readonly showOwnerLabel: boolean;  readonly onEdit: () => void;  readonly onReconnect: () => void;  readonly onRemove: () => void;}) {  const { connection, needsReconsent } = props;  const [checking, setChecking] = useState(false);   // The status renders WITHOUT any clicking: every checkHealth run persists its  // verdict on the connection row, so the list answers "has this expired?" at a  // glance. A live check from this session takes precedence. We deliberately do  // NOT derive expiry from the stored `expiresAt`: that's the access-token  // lifetime, which refreshes, so a passive countdown means nothing.  //  // Health checks are AUTOMATIC: the hook revalidates on mount, stale-while-  // revalidate style (shared with the integrations-list summary), and the  // persisted verdict renders instantly while the probe corrects it in place.  const { probe, status, runCheck } = useConnectionHealth(connection);  const indicator = HEALTH_INDICATOR_COLOR[status];   // Prefer a probed identity (the live account), then the stored label, then the  // connection name. The probe is the whole point: it shows WHICH account this is.  const identity =    (probe?.identity && probe.identity.length > 0 ? probe.identity : null) ??    (connection.identityLabel && connection.identityLabel.length > 0      ? connection.identityLabel      : null);  const displayLabel = identity ?? String(connection.name);   const expired = status === "expired";   const handleCheck = async () => {    if (checking) return;    setChecking(true);    const exit = await runCheck();    setChecking(false);    if (Exit.isFailure(exit)) {      toast.error(messageFromExit(exit, "Health check failed"));      return;    }    // The hook already folded the fresh probe into the live state.    if (exit.value.status === "healthy") {      toast.success(        exit.value.identity ? `Healthy: ${exit.value.identity}` : "Connection is healthy",      );    } else if (exit.value.status === "expired") {      toast.error("Connection expired, reconnect to restore access");    } else if (exit.value.status === "degraded") {      toast.warning(exit.value.detail ?? "Connection check returned an error");    } else {      toast.message("No health check is configured for this integration");    }  };   return (    <CardStackEntry className="flex-wrap items-start">      <CardStackEntryContent>        <CardStackEntryTitle className="flex min-w-0 items-center gap-2">          <span            aria-label={`Status: ${HEALTH_STATUS_LABEL[status]}`}            title={HEALTH_STATUS_LABEL[status]}            className={`size-2 shrink-0 rounded-full ${indicator.dot}`}          />          <span className="truncate">{displayLabel}</span>          {expired ? (            <Badge variant="destructive" className="shrink-0">              Expired            </Badge>          ) : null}          {needsReconsent ? (            <Badge variant="outline" className="shrink-0 border-border text-muted-foreground">              Reconnect to grant access            </Badge>          ) : null}        </CardStackEntryTitle>        {connection.description && connection.description.length > 0 ? (          <CardStackEntryDescription className="mt-1 text-xs">            {connection.description}          </CardStackEntryDescription>        ) : null}        {needsReconsent ? (          <CardStackEntryDescription className="mt-1 text-xs text-muted-foreground">            This connection wasn't granted all the access this integration now needs.          </CardStackEntryDescription>        ) : null}      </CardStackEntryContent>      <CardStackEntryActions className="self-start pt-0.5">        {props.showOwnerLabel ? (          <Badge variant="outline">{ownerLabel(connection.owner)}</Badge>        ) : null}        <DropdownMenu>          <DropdownMenuTrigger asChild>            <Button              variant="ghost"              size="icon"              className="size-7 opacity-0 transition-opacity group-hover/card-stack-entry:opacity-100 group-focus-within/card-stack-entry:opacity-100 data-[state=open]:opacity-100"            >              <svg viewBox="0 0 16 16" className="size-3">                <circle cx="8" cy="3" r="1.2" fill="currentColor" />                <circle cx="8" cy="8" r="1.2" fill="currentColor" />                <circle cx="8" cy="13" r="1.2" fill="currentColor" />              </svg>            </Button>          </DropdownMenuTrigger>          <DropdownMenuContent align="end" className="w-40">            <DropdownMenuItem              className="text-sm"              disabled={checking}              onClick={() => void handleCheck()}            >              {checking ? "Checking…" : "Check now"}            </DropdownMenuItem>            <DropdownMenuItem className="text-sm" onClick={props.onEdit}>              Edit            </DropdownMenuItem>            <DropdownMenuItem className="text-sm" onClick={props.onReconnect}>              Reconnect            </DropdownMenuItem>            <DropdownMenuItem variant="destructive" className="text-sm" onClick={props.onRemove}>              Remove            </DropdownMenuItem>          </DropdownMenuContent>        </DropdownMenu>      </CardStackEntryActions>    </CardStackEntry>  );} function OwnerAccounts(props: {  readonly integration: IntegrationSlug;  readonly owner: Owner;  readonly showOwnerLabels: boolean;  readonly methods: readonly AuthMethod[];  readonly onEdit: (connection: Connection) => void;  readonly onDcrReconnect: (connection: Connection) => void;  /** The integration's declared oauth scopes — compared against each connection's   *  granted `oauthScope` to flag connections that must reconnect for new access. */  readonly declaredScopes: readonly string[] | undefined;}) {  const { integration, owner } = props;  const connections = useAtomValue(connectionsForIntegrationAtom({ integration, owner }));  const doRemove = useAtomSet(removeConnectionOptimistic(owner), {    mode: "promiseExit",  });  const doRefresh = useAtomSet(refreshConnection, { mode: "promiseExit" });  const doStartOAuth = useAtomSet(startOAuth, { mode: "promiseExit" });  // OAuth connections re-CONSENT on Reconnect (a token refresh cannot widen  // scopes and fails with no refresh token), so they re-run the OAuth flow. The  // popup flow re-mints the SAME connection (owner/integration/name) with a  // fresh refresh token + the widened scope union. Static creds keep the refresh  // path. One flow hosted per owner-group is enough — Reconnect is one-at-a-time.  const oauthPopup = useOAuthPopupFlow({    popupName: "reconnect-oauth",    detectPopupClosed: false,    startErrorMessage: "Failed to reconnect",  });   const rows: readonly Connection[] = AsyncResult.isSuccess(connections) ? connections.value : [];  if (rows.length === 0) return null;   const handleReconnect = async (connection: Connection) => {    // OAuth connection → re-run the OAuth flow (re-consent + widened scopes +    // fresh refresh token); re-minting overwrites the existing connection.    if (reconnectMode(connection) === "oauth") {      const method = props.methods.find(        (candidate: AuthMethod) =>          candidate.kind === "oauth" && String(candidate.template) === String(connection.template),      );      if (        method?.oauth?.supportsDynamicRegistration === true ||        method?.oauth?.discoveryUrl != null      ) {        props.onDcrReconnect(connection);        return;      }      const payload = oauthReconnectPayload(connection);      if (payload === null) return;      // `oauth.start` discriminates the grant: client_credentials mints inline      // (`status: "connected"`, no authorization URL) while authorization_code      // returns a redirect the popup must complete. The popup hook only handles      // the redirect grant (a null authorization URL is an error there), so we      // start once here and branch — inline-connected is handled directly,      // redirect hands the already-issued URL to the popup. Both re-mint the      // SAME connection (owner/integration/name).      const startExit = await doStartOAuth({        payload,        reactivityKeys: connectionWriteKeys,      });      if (Exit.isFailure(startExit)) {        toast.error(messageFromExit(startExit, "Failed to reconnect"));        trackEvent("connection_reconnected", {          integration_slug: String(connection.integration),          owner: connection.owner,          success: false,        });        return;      }      const started = startExit.value;      if (started.status === "connected") {        toast.success("Reconnected");        trackEvent("connection_reconnected", {          integration_slug: String(connection.integration),          owner: connection.owner,          success: true,        });        return;      }      void oauthPopup.openAuthorization({        owner: payload.owner,        run: () =>          Promise.resolve({            state: started.state,            authorizationUrl: started.authorizationUrl,          }),        onSuccess: () => {          toast.success("Reconnected");          trackEvent("connection_reconnected", {            integration_slug: String(connection.integration),            owner: connection.owner,            success: true,          });        },        onError: () => {          toast.error("Failed to reconnect");          trackEvent("connection_reconnected", {            integration_slug: String(connection.integration),            owner: connection.owner,            success: false,          });        },      });      return;    }    // Non-OAuth connection → token refresh (the original path).    const exit = await doRefresh({      params: {        owner: connection.owner,        integration: connection.integration,        name: connection.name,      },      reactivityKeys: connectionWriteKeys,    });    trackEvent("connection_reconnected", {      integration_slug: String(connection.integration),      owner: connection.owner,      success: Exit.isSuccess(exit),    });    if (Exit.isFailure(exit)) {      toast.error(messageFromExit(exit, "Failed to reconnect"));    }  };   const handleRemove = async (connection: Connection) => {    const exit = await doRemove({      params: {        owner: connection.owner,        integration: connection.integration,        name: connection.name,      },      reactivityKeys: connectionWriteKeys,    });    trackEvent("connection_removed", {      integration_slug: String(connection.integration),      owner: connection.owner,      success: Exit.isSuccess(exit),    });    if (Exit.isFailure(exit)) {      toast.error(messageFromExit(exit, "Failed to remove connection"));    }  };   return (    <CardStack>      {props.showOwnerLabels ? <CardStackHeader>{ownerLabel(owner)}</CardStackHeader> : null}      <CardStackContent>        {rows.map((connection: Connection) => (          <AccountRow            key={`${connection.owner}:${connection.integration}:${connection.name}`}            connection={connection}            needsReconsent={connectionNeedsReconsent(connection, props.declaredScopes)}            showOwnerLabel={props.showOwnerLabels}            onEdit={() => props.onEdit(connection)}            onReconnect={() => void handleReconnect(connection)}            onRemove={() => void handleRemove(connection)}          />        ))}      </CardStackContent>    </CardStack>  );} type ProviderAccountForServices = ProviderAccount<Connection, EnableServiceIntegration>; const connectedServiceSlugs = (account: ProviderAccountForServices): ReadonlySet<string> =>  new Set(account.connections.map((entry) => String(entry.integration.slug))); const missingServicesFor = (  account: ProviderAccountForServices,  integrations: readonly EnableServiceIntegration[],): readonly EnableServiceIntegration[] => {  const connected = connectedServiceSlugs(account);  return integrations.filter((integration) => !connected.has(String(integration.slug)));}; export function ProviderAccountsSection(props: { readonly family: string }) {  const accountsResult = useAtomValue(providerAccountsAtom);  const integrationsResult = useAtomValue(integrationsAtom);  const ownerDisplay = useOwnerDisplay();  const [selectedAccount, setSelectedAccount] = useState<ProviderAccountForServices | null>(null);   if (!MULTI_SERVICE_FAMILIES.has(props.family)) return null;   const integrations: readonly EnableServiceIntegration[] = AsyncResult.isSuccess(    integrationsResult,  )    ? (integrationsResult.value as readonly EnableServiceIntegration[])        .filter((integration) => integration.kind === props.family)        .sort((a, b) => a.name.localeCompare(b.name))    : [];   const accounts: readonly ProviderAccountForServices[] = AsyncResult.isSuccess(accountsResult)    ? (accountsResult.value as readonly ProviderAccountForServices[]).filter(        (account) => account.family === props.family,      )    : [];   if (!AsyncResult.isSuccess(accountsResult) || !AsyncResult.isSuccess(integrationsResult)) {    return (      <section className="space-y-3">        <h3 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">          Provider accounts        </h3>        <div className="flex items-center gap-2 py-3">          <div className="size-1.5 animate-pulse rounded-full bg-muted-foreground/30" />          <p className="text-sm text-muted-foreground">Loading provider accounts...</p>        </div>      </section>    );  }   if (accounts.length === 0) return null;   const modalIntegrations = selectedAccount    ? missingServicesFor(selectedAccount, integrations)    : [];   return (    <section className="space-y-3">      <div>        <h3 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">          Provider accounts        </h3>      </div>      <CardStack>        <CardStackContent>          {accounts.map((account) => {            const missing = missingServicesFor(account, integrations);            const canAdd =              missing.some((integration) =>                integration.authMethods.some((method) => method.kind === "oauth"),              ) && normalizeEmail(account.label) !== null;            return (              <CardStackEntry key={account.identityKey} className="flex-wrap items-start gap-3">                <CardStackEntryContent>                  <CardStackEntryTitle className="flex min-w-0 items-center gap-2">                    <span className="truncate">{account.label}</span>                    {ownerDisplay.showOwnerLabels ? (                      <Badge variant="outline">{ownerLabel(account.owner as Owner)}</Badge>                    ) : null}                  </CardStackEntryTitle>                  <CardStackEntryDescription className="mt-1 text-xs">                    {account.connections.length} connected{" "}                    {account.connections.length === 1 ? "service" : "services"}                  </CardStackEntryDescription>                  <div className="mt-3 divide-y divide-border rounded-md border border-border">                    {account.connections.map((entry) => (                      <div                        key={`${entry.connection.owner}:${String(entry.integration.slug)}:${String(                          entry.connection.name,                        )}`}                        className="flex items-center justify-between gap-3 px-3 py-2"                      >                        <div className="min-w-0">                          <p className="truncate text-sm font-medium text-foreground">                            {entry.integration.name}                          </p>                          <p className="truncate text-xs text-muted-foreground">                            {entry.connection.identityLabel || String(entry.connection.name)}                          </p>                        </div>                        <Badge variant="outline">{String(entry.connection.name)}</Badge>                      </div>                    ))}                  </div>                </CardStackEntryContent>                <CardStackEntryActions className="self-start pt-0.5">                  <Button                    type="button"                    variant="outline"                    size="sm"                    disabled={!canAdd}                    onClick={() => setSelectedAccount(account)}                  >                    <PlusIcon />                    Add services                  </Button>                </CardStackEntryActions>              </CardStackEntry>            );          })}        </CardStackContent>      </CardStack>       {selectedAccount ? (        <EnableServicesModal          open={true}          account={selectedAccount}          integrations={modalIntegrations}          onOpenChange={(open) => {            if (!open) setSelectedAccount(null);          }}        />      ) : null}    </section>

Lines 1–500 of 674.