3398 lines · 150.0 KB · TypeScript
RawDownload
import { useCallback, useEffect, useMemo, useRef, useState } from "react";import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react";import * as Exit from "effect/Exit";import * as Option from "effect/Option";import * as Predicate from "effect/Predicate";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import {  ConnectionName,  IntegrationSlug,  OAuthClientSlug,  ProviderItemId,  ProviderKey,  identityPathTier,  rankResponseSample,  type HealthCheckCandidate,  type HealthCheckResult,  type HealthCheckSpec,  type OAuthClientSummary,  type OAuthProbeResult,  type Owner,} from "@executor-js/sdk/shared";import type { IntegrationAccountHandoff } from "@executor-js/sdk/client";import { toast } from "sonner"; import {  addConnectionOptimistic,  checkConnectionHealth,  connectionsAllAtom,  createOAuthClientOptimistic,  integrationHealthCheckAtom,  integrationHealthCheckCandidatesAtom,  oauthClientsOptimisticAtom,  probeOAuth,  providerItemsAtom,  providersAtom,  registerDynamicOAuthClient,  removeOAuthClientOptimistic,  setIntegrationHealthCheck,  startOAuth,  updateConnection,  validateConnection,} from "../api/atoms";import {  connectionCheckKeys,  connectionWriteKeys,  healthCheckWriteKeys,  oauthClientWriteKeys,} from "../api/reactivity-keys";import { HEALTH_INDICATOR_COLOR, HEALTH_STATUS_LABEL } from "../lib/health-display";import { FreeformCombobox, type FreeformComboboxOption } from "./combobox";import { messageFromExit } from "../api/error-reporting";import { trackEvent } from "../api/analytics";import { useOrganizationId } from "../api/organization-context";import { ownerLabel, ownerLabelForHost, useOwnerDisplay } from "../api/owner-display";import {  ConnectionOwnerDropdown,  connectionOwnerOptionsForHost,  defaultConnectionOwnerForHost,  normalizeConnectionOwner,  resolveOAuthConnectionOwnerForHost,  type ConnectionOwnerOption,} from "../plugins/connection-owner";import {  oauthCallbackUrl,  oauthClientIdMetadataDocumentUrl,  useOAuthPopupFlow,  type OAuthCompletionPayload,  type OAuthPopupReservation,} from "../plugins/oauth-sign-in";import {  clientDisplayName,  clientHost,  optimisticDcrClientSlug,  selectDcrClientsForIntegration,  uniqueClientSlug,  useOAuthClientsForIntegration,  type OAuthClientOption,} from "../plugins/use-effective-oauth-client";import { cn } from "../lib/utils";import { buildUsageMap, connectionsUsingClient } from "../lib/oauth-client-usage";import {  OAuthClientForm,  registrationScopes,  type OAuthClientFormPrefill,} from "./oauth-client-form";import { RemoveOAuthAppDialog } from "./remove-oauth-app-dialog";import { AddCustomMethodForm, type CreateCustomMethod } from "./add-custom-method-modal";import { PlacementLine, type AuthMethod } from "../lib/auth-placements";import { connectionIdentifier } from "../lib/connection-name";import { Badge } from "./badge";import { Button } from "./button";import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./collapsible";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuItem,  DropdownMenuTrigger,} from "./dropdown-menu";import { ChevronDown, EyeIcon, EyeOffIcon, PlusIcon, XIcon } from "lucide-react";import {  Dialog,  DialogContent,  DialogDescription,  DialogFooter,  DialogHeader,  DialogTitle,} from "./dialog";import { Input } from "./input";import { Label } from "./label";import { RadioGroup, RadioGroupItem } from "./radio-group";import {  Combobox,  ComboboxContent,  ComboboxEmpty,  ComboboxInput,  ComboboxItem,  ComboboxList,} from "./combobox";import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs"; // ---------------------------------------------------------------------------// Add-account modal — the connection-create form.//// Field order: (1) display name · (2) authentication method · (3) credential// · (4) saved-to owner. A connection is immutable once created. Step 2 collects// one value per distinct input the method declares — usually one, but a// multi-input method (e.g. Datadog's two keys) shows one field per variable.//// OAuth: step 2 lists the registered apps usable for this integration and lets// you PICK one (or "Register a new app"). While registering, name/saved-to are// hidden (they don't apply yet). Once an app is selected, the footer's "Connect// with OAuth" mints the connection with the name + saved-to. The CLIENT owner// (whose app) is distinct from the CONNECTION's saved-to owner.// --------------------------------------------------------------------------- const ONEPASSWORD_PROVIDER = ProviderKey.make("onepassword"); type CredentialOrigin = "paste" | "onepassword";type CredentialInput = { readonly variable: string; readonly label: string }; type CredentialPayloadOrigin =  | { readonly values: Record<string, string> }  | {      readonly from: {        readonly provider: ProviderKey;        readonly id: ProviderItemId;      };    }; export function createCredentialPayloadOrigin(args: {  readonly origin: CredentialOrigin;  readonly inputs: readonly CredentialInput[];  readonly values: Readonly<Record<string, string>>;  readonly onePasswordItemId: string;  readonly singleInput: boolean;}): CredentialPayloadOrigin | null {  if (args.inputs.length === 0) return { values: { token: "" } };  if (args.origin === "onepassword") {    const id = args.onePasswordItemId.trim();    if (!args.singleInput || id.length === 0) return null;    return {      from: { provider: ONEPASSWORD_PROVIDER, id: ProviderItemId.make(id) },    };  }   const values = Object.fromEntries(    args.inputs.map((input) => [input.variable, (args.values[input.variable] ?? "").trim()]),  );  return Object.values(values).every((value) => value.length > 0) ? { values } : null;} const numberBadge = (n: number) => (  <span className="inline-grid size-[18px] shrink-0 place-items-center rounded-full border border-border bg-muted text-[11px] text-muted-foreground">    {n}  </span>); function isOnePasswordRegistered(  providers: AsyncResult.AsyncResult<readonly ProviderKey[], unknown>,) {  return AsyncResult.match(providers, {    onInitial: () => false,    onFailure: () => false,    onSuccess: ({ value }) =>      value.some((provider: ProviderKey) => String(provider) === String(ONEPASSWORD_PROVIDER)),  });} /** Constant-label reveal control (aria-pressed carries the state, per the *  screen-reader guidance against relabeling toggles). Masked keys make typos *  invisible; every credential field pairs with one of these. */function RevealKeyButton(props: { readonly revealed: boolean; readonly onToggle: () => void }) {  return (    <Button      type="button"      variant="ghost"      size="icon-sm"      aria-label="Show key"      aria-pressed={props.revealed}      className="shrink-0 text-muted-foreground hover:bg-transparent hover:text-foreground"      onClick={props.onToggle}    >      {props.revealed ? <EyeOffIcon /> : <EyeIcon />}    </Button>  );} function PasteCredentialInputs(props: {  readonly inputs: readonly CredentialInput[];  readonly singleInput: boolean;  /** Force the per-input label even for a single input (env vars name their   *  field rather than relying on a generic "value / token" placeholder). */  readonly showLabels?: boolean;  /** Single-input only: the placement's lead + prefix (e.g. "Authorization:   *  Bearer "), rendered as a non-editable affix INSIDE the field so the input   *  reads as the header value being built. Replaces the separate preview line. */  readonly affix?: string | null;  readonly values: Readonly<Record<string, string>>;  readonly onChange: (values: Record<string, string>) => void;}) {  // One reveal state for the whole group: a user checking their paste wants  // to see all of it, not toggle per field.  const [revealed, setRevealed] = useState(false);  const inputType = revealed ? "text" : "password";  if (!props.singleInput) {    return (      <div className="grid gap-3 sm:grid-cols-2">        {props.inputs.map((input) => (          <div key={input.variable} className="min-w-0 space-y-1.5">            <div className="flex min-w-0 items-center gap-2">              <Label                htmlFor={`credential-input-${input.variable}`}                className="min-w-0 truncate font-mono text-xs font-medium text-muted-foreground"              >                {input.label}              </Label>            </div>            <div className="flex items-center gap-1">              <Input                id={`credential-input-${input.variable}`}                type={inputType}                autoComplete="off"                data-1p-ignore                data-lpignore="true"                data-bwignore                data-form-type="other"                value={props.values[input.variable] ?? ""}                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>                  props.onChange({                    ...props.values,                    [input.variable]: e.target.value,                  })                }                className="h-9 font-mono text-sm"                data-ph-block              />              <RevealKeyButton revealed={revealed} onToggle={() => setRevealed((v) => !v)} />            </div>          </div>        ))}      </div>    );  }   // Past the multi-input grid above, singleInput is always true, so labelled  // reduces to showLabels: env vars opt in to naming their single field.  const labelled = props.showLabels ?? false;  return (    <div className="space-y-2">      {props.inputs.map((input) => (        <div key={input.variable} className="space-y-1">          {labelled && (            <Label className="font-mono text-xs text-muted-foreground">{input.label}</Label>          )}          {props.affix ? (            // Merged field: the placement's lead + prefix is a FIXED addon (its            // own muted segment, divider, non-selectable), and the user types            // only the token after it — the field reads as the header value being            // built. Only the border reacts to focus (no full-field glow). The            // autoComplete/ignore attrs stop the browser and password managers            // from offering to GENERATE a password here; the app's own 1Password            // picker covers filling an existing secret.            <div className="flex h-9 w-full min-w-0 items-stretch overflow-hidden rounded-md border border-input bg-transparent font-mono text-sm shadow-xs transition-colors focus-within:border-ring dark:bg-input/30">              {/* min-w-0 + inner ellipsis: a method saved with a very long                  prefix must truncate inside the field, not stretch it. */}              <span className="flex min-w-0 max-w-[60%] select-none items-center border-r border-input bg-muted/40 px-3 text-muted-foreground/70">                <span className="overflow-hidden text-ellipsis whitespace-pre">{props.affix}</span>              </span>              {/* oxlint-disable-next-line react/forbid-elements */}              <input                type={inputType}                autoComplete="off"                // The key is the modal's real first input: the display name is                // derived from it, so this is where typing starts.                // oxlint-disable-next-line jsx_a11y/no-autofocus -- deliberate: the credential drives everything else in the form                autoFocus                data-1p-ignore                data-lpignore="true"                data-bwignore                data-form-type="other"                // No visible label or placeholder here (the affix carries the                // instruction visually), so the accessible name comes from                // aria-label instead, matching the multi-input grid's Label                // text one-for-one.                aria-label={input.label}                value={props.values[input.variable] ?? ""}                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>                  props.onChange({ ...props.values, [input.variable]: e.target.value })                }                className="min-w-0 flex-1 bg-transparent px-3 outline-none"                data-ph-block              />              <RevealKeyButton revealed={revealed} onToggle={() => setRevealed((v) => !v)} />            </div>          ) : (            <div className="flex items-center gap-1">              <Input                type={inputType}                autoComplete="off"                data-1p-ignore                data-lpignore="true"                data-bwignore                data-form-type="other"                // The Label above (when labelled) has no htmlFor, so it isn't                // programmatically associated with this input; aria-label is                // the only accessible name in either case.                aria-label={input.label}                value={props.values[input.variable] ?? ""}                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>                  props.onChange({                    ...props.values,                    [input.variable]: e.target.value,                  })                }                className="font-mono"                data-ph-block              />              <RevealKeyButton revealed={revealed} onToggle={() => setRevealed((v) => !v)} />            </div>          )}        </div>      ))}    </div>  );} type OnePasswordItem = {  readonly id: ProviderItemId;  readonly name: string;  readonly group?: string;}; function OnePasswordItemSelect(props: {  readonly value: string;  readonly onChange: (value: string) => void;}) {  const itemsAtom = providerItemsAtom(ONEPASSWORD_PROVIDER);  const itemsResult = useAtomValue(itemsAtom);  const refreshItems = useAtomRefresh(itemsAtom);   // Stale-while-revalidate: with a retained value the list renders instantly  // and one background refresh per mount picks up vault changes (refreshing  // keeps the previous value, so nothing flashes). A cold mount is already  // fetching — refreshing it would only restart the request. The ref carries  // the latest cached-ness into the effect without re-running it.  const isCachedRef = useRef(false);  isCachedRef.current = AsyncResult.isSuccess(itemsResult);  useEffect(() => {    if (isCachedRef.current) refreshItems();  }, [refreshItems]);  const state = AsyncResult.matchWithError(    itemsResult as AsyncResult.AsyncResult<readonly OnePasswordItem[], Error>,    {      onInitial: () => ({        items: [] as readonly OnePasswordItem[],        loading: true,        error: null as string | null,      }),      onError: () => ({        items: [] as readonly OnePasswordItem[],        loading: false,        error: "Failed to load 1Password items",      }),      onDefect: () => ({        items: [] as readonly OnePasswordItem[],        loading: false,        error: "Failed to load 1Password items",      }),      onSuccess: ({ value }) => ({ items: value, loading: false, error: null }),    },  );   const stateItems = state.items;  const sorted = useMemo(    () =>      [...stateItems].sort(        (a, b) => a.name.localeCompare(b.name) || (a.group ?? "").localeCompare(b.group ?? ""),      ),    [stateItems],  );  const byId = useMemo(() => {    const map = new Map<string, OnePasswordItem>();    for (const item of sorted) map.set(String(item.id), item);    return map;  }, [sorted]);  const ids = useMemo(() => sorted.map((item) => String(item.id)), [sorted]);   const filter = useCallback(    (id: string, query: string) => {      const needle = query.trim().toLowerCase();      if (needle === "") return true;      const item = byId.get(id);      return [item?.name ?? "", item?.group ?? ""].some((part) =>        part.toLowerCase().includes(needle),      );    },    [byId],  );   if (state.loading) {    return <p className="text-xs text-muted-foreground">Loading 1Password items…</p>;  }  if (state.error) {    return <p className="text-xs text-destructive">{state.error}</p>;  }  if (state.items.length === 0) {    return <p className="text-xs text-muted-foreground">No 1Password items found.</p>;  }   return (    <div className="space-y-1" data-ph-block>      <Combobox        items={ids}        value={props.value.length > 0 ? props.value : null}        filter={filter}        limit={100}        itemToStringLabel={(id: string) => byId.get(id)?.name ?? id}        onValueChange={(id) => {          if (id !== null) props.onChange(id);        }}      >        <ComboboxInput placeholder="Search 1Password items…" className="w-full" />        <ComboboxContent>          <ComboboxEmpty>No items match.</ComboboxEmpty>          <ComboboxList>            {(id: string) => {              const item = byId.get(id);              return (                <ComboboxItem key={id} value={id}>                  <span className="min-w-0 flex-1 truncate">{item?.name ?? id}</span>                  {item?.group && (                    <span className="shrink-0 font-mono text-[11px] text-muted-foreground">                      {item.group}                    </span>                  )}                </ComboboxItem>              );            }}          </ComboboxList>        </ComboboxContent>      </Combobox>    </div>  );} function CredentialValueFields(props: {  readonly inputs: readonly CredentialInput[];  readonly singleInput: boolean;  readonly showLabels?: boolean;  /** Single-input only: placement lead + prefix to merge into the field. */  readonly affix?: string | null;  /** Allow an external provider (1Password) origin. Off for env methods: the   *  wire's external `from` is keyed under the canonical `token` variable, but   *  an env credential must be keyed under its env var name, so a 1Password   *  origin would persist the secret under the wrong key and the subprocess   *  would launch without it. Paste stays correctly keyed per variable. */  readonly allowExternalProvider?: boolean;  readonly values: Readonly<Record<string, string>>;  readonly onValuesChange: (values: Record<string, string>) => void;  readonly origin: CredentialOrigin;  readonly onOriginChange: (origin: CredentialOrigin) => void;  readonly onePasswordItemId: string;  readonly onOnePasswordItemIdChange: (value: string) => void;}) {  const providers = useAtomValue(providersAtom);  const onePasswordAvailable =    props.singleInput &&    (props.allowExternalProvider ?? true) &&    isOnePasswordRegistered(providers);   return (    <div className="space-y-3">      {onePasswordAvailable ? (        <RadioGroup          value={props.origin}          onValueChange={(value) => props.onOriginChange(value as CredentialOrigin)}          className="grid grid-cols-2 gap-2"        >          <Label            htmlFor="credential-origin-paste"

Lines 1–500 of 3398.