2929 lines · 129.8 KB · TypeScript
RawDownload
import { useCallback, useEffect, useMemo, useRef, useState } from "react";import { useAtomSet, useAtomValue } from "@effect/atom-react";import * as Exit from "effect/Exit";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 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,} 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";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>  );} function OnePasswordItemSelect(props: {  readonly value: string;  readonly onChange: (value: string) => void;}) {  const itemsResult = useAtomValue(providerItemsAtom(ONEPASSWORD_PROVIDER));  const state = AsyncResult.matchWithError(    itemsResult as AsyncResult.AsyncResult<      readonly { readonly id: ProviderItemId; readonly name: string }[],      Error    >,    {      onInitial: () => ({        items: [] as readonly {          readonly id: ProviderItemId;          readonly name: string;        }[],        loading: true,        error: null as string | null,      }),      onError: () => ({        items: [] as readonly {          readonly id: ProviderItemId;          readonly name: string;        }[],        loading: false,        error: "Failed to load 1Password items",      }),      onDefect: () => ({        items: [] as readonly {          readonly id: ProviderItemId;          readonly name: string;        }[],        loading: false,        error: "Failed to load 1Password items",      }),      onSuccess: ({ value }) => ({ items: value, loading: false, error: null }),    },  );   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>      <Select value={props.value} onValueChange={props.onChange}>        <SelectTrigger>          <SelectValue placeholder="Select secret" />        </SelectTrigger>        <SelectContent>          {state.items.map((item) => (            <SelectItem key={String(item.id)} value={String(item.id)}>              {item.name}            </SelectItem>          ))}        </SelectContent>      </Select>    </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"            className="flex cursor-pointer items-center gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-sm font-normal has-[:checked]:border-ring has-[:checked]:bg-accent/40"          >            <RadioGroupItem id="credential-origin-paste" value="paste" />            Paste value          </Label>          <Label            htmlFor="credential-origin-onepassword"            className="flex cursor-pointer items-center gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-sm font-normal has-[:checked]:border-ring has-[:checked]:bg-accent/40"          >            <RadioGroupItem id="credential-origin-onepassword" value="onepassword" />            1Password          </Label>        </RadioGroup>      ) : null}       {onePasswordAvailable && props.origin === "onepassword" ? (        <OnePasswordItemSelect          value={props.onePasswordItemId}          onChange={props.onOnePasswordItemIdChange}        />      ) : (        <PasteCredentialInputs          inputs={props.inputs}          singleInput={props.singleInput}          showLabels={props.showLabels}          affix={props.affix}          values={props.values}          onChange={props.onValuesChange}        />      )}    </div>  );} // ---------------------------------------------------------------------------// Step header — the label introducing each section of the form. Four style// variants are kept for design review; flip STEP_HEADER_VARIANT to preview each// one in isolation. The numbered-circle treatment implied a sequential wizard// the form isn't (every section shows at once), so the alternatives drop the// numbers.//   - "numbered": numbered circle + label + inline hint (current).//   - "eyebrow":  uppercase micro-caps, hint on its own line. Matches the app's//                 existing section headers (e.g. AccountsSection).//   - "sentence": plain form label in foreground weight, hint inline.//   - "accent":   a short leading rule for rhythm without implied sequence.// ---------------------------------------------------------------------------type StepHeaderVariant = "numbered" | "eyebrow" | "sentence" | "accent"; /** Swap this to preview each step-header style. */const STEP_HEADER_VARIANT: StepHeaderVariant = "eyebrow"; function StepHeader(props: {  readonly index: number;  readonly label: string;  readonly hint?: string;  readonly htmlFor?: string;}) {  const { index, label, hint, htmlFor } = props;   const variants: Record<StepHeaderVariant, React.ReactElement> = {    numbered: (      <Label htmlFor={htmlFor} className="text-xs text-muted-foreground">        {numberBadge(index)}

Lines 1–500 of 2929.