import { useMemo, useState, Suspense } from "react"; import { useAtomValue, useAtomSet } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import * as Exit from "effect/Exit"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { toast } from "sonner"; import { removeSecretOptimistic, secretsOptimisticAtom, secretUsagesAtom } from "../api/atoms"; import { secretWriteKeys } from "../api/reactivity-keys"; import { useSecretProviderPlugins } from "@executor-js/sdk/client"; import { SecretId, SecretInUseError, type ScopeId } from "@executor-js/sdk"; import { SecretForm } from "../plugins/secret-form"; import { useScope } from "../hooks/use-scope"; import { useScopeStack } from "../api/scope-context"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, } from "../components/dialog"; import { Button } from "../components/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../components/dropdown-menu"; import { CardStack, CardStackContent, CardStackEntry, CardStackEntryActions, CardStackEntryContent, CardStackEntryDescription, CardStackEntryTitle, CardStackHeader, } from "../components/card-stack"; import { Badge } from "../components/badge"; type SecretStorageOption = { readonly label: string; readonly value: string; }; const defaultStorageOptions: readonly SecretStorageOption[] = [ { value: "auto", label: "Auto" }, { value: "keychain", label: "Keychain" }, { value: "file", label: "File" }, ]; const isSecretInUseError = Schema.is(SecretInUseError); // --------------------------------------------------------------------------- // Add secret dialog // // Form state, derived id, dup detection, and submit lifecycle live in // `` and are shared with the inline create flow in // secret-header-auth.tsx. Dialog content remounts on each open via `key` so // state always starts fresh — no manual reset. // --------------------------------------------------------------------------- function AddSecretDialog(props: { open: boolean; onOpenChange: (v: boolean) => void; description: string; storageOptions: readonly SecretStorageOption[]; existingSecretIds: readonly string[]; scopeId: ScopeId; }) { return ( {props.open && ( props.onOpenChange(false)} /> )} ); } function AddSecretDialogContent(props: { description: string; storageOptions: readonly SecretStorageOption[]; existingSecretIds: readonly string[]; scopeId: ScopeId; onClose: () => void; }) { const initialProvider = props.storageOptions[0]?.value ?? "auto"; return ( New secret {props.description}
Save secret
); } // --------------------------------------------------------------------------- // Used-by footer — fetched per-secret. Keeps the list compact: shows the // count plus the first few owner names, with a "+N more" tail. Empty // state collapses to nothing so secrets that aren't referenced anywhere // don't get a noisy "Used by 0" line. // --------------------------------------------------------------------------- function SecretUsageFooter(props: { scopeId: ScopeId; secretId: SecretId }) { const usages = useAtomValue(secretUsagesAtom(props.scopeId, props.secretId)); return AsyncResult.match(usages, { onInitial: () => null, onFailure: () => null, onSuccess: ({ value }) => { if (value.length === 0) return null; const labels = value .map((u) => u.ownerName ?? u.ownerId) .filter((s, i, a) => a.indexOf(s) === i); const visible = labels.slice(0, 3); const hidden = labels.length - visible.length; return ( Used by {visible.join(", ")} {hidden > 0 ? ` +${hidden} more` : ""} ); }, }); } // --------------------------------------------------------------------------- // Secret row // --------------------------------------------------------------------------- function SecretRow(props: { usageScopeId: ScopeId; showProvider: boolean; secret: { id: string; scopeId: ScopeId; name: string; provider?: string }; scopeLabel: string; onRemove: () => void; }) { const { secret, showProvider } = props; return ( {secret.name} {secret.id} {props.scopeLabel} {showProvider && secret.provider && {secret.provider}} Remove secret ); } // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export function SecretsPage(props: { addSecretDescription?: string; showProviderInfo?: boolean; storageOptions?: readonly SecretStorageOption[]; }) { const storageOptions = props.storageOptions ?? defaultStorageOptions; const showProviderInfo = props.showProviderInfo ?? true; const addSecretDescription = props.addSecretDescription ?? "Store a credential or API key. Values are kept in your system keychain when available, with a local encrypted file fallback."; const secretProviderPlugins = useSecretProviderPlugins(); const [addOpen, setAddOpen] = useState(false); const scopeId = useScope(); const scopeStack = useScopeStack(); const secrets = useAtomValue(secretsOptimisticAtom(scopeId)); const scopeLabel = (secretScopeId: ScopeId): string => { const index = scopeStack.findIndex((entry) => entry.id === secretScopeId); if (index === 0) return "Personal"; if (index > 0) return scopeStack[index]?.name || "Organization"; return "Scoped"; }; const existingSecretIds = useMemo( () => AsyncResult.match(secrets, { onInitial: () => [] as string[], onFailure: () => [] as string[], onSuccess: ({ value }) => value.map((secret) => secret.id), }), [secrets], ); const doRemove = useAtomSet(removeSecretOptimistic(scopeId), { mode: "promiseExit", }); const handleRemove = async (secret: { readonly id: string; readonly scopeId: ScopeId }) => { const exit = await doRemove({ params: { scopeId: secret.scopeId, secretId: SecretId.make(secret.id), }, reactivityKeys: secretWriteKeys, }); if (Exit.isFailure(exit)) { const error = Exit.findErrorOption(exit); if (Option.isSome(error) && isSecretInUseError(error.value)) { const count = error.value.usageCount; toast.error( `Secret is used by ${count} ${count === 1 ? "source" : "sources"}. Detach it before removing it.`, ); } else { toast.error("Failed to remove secret"); } } }; return (
{/* Header */}

Secrets

Credentials and API keys used by your connected sources.

{/* Provider plugins */} {showProviderInfo && secretProviderPlugins.length > 0 && (
Providers {secretProviderPlugins.map((plugin) => (
} >
))}
)} {/* Secrets list */} {AsyncResult.match(secrets, { onInitial: () => (

Loading secrets…

), onFailure: () => (

Failed to load secrets

), onSuccess: ({ value }) => ( Secrets {value.length === 0 ? ( Add API keys and credentials to authenticate your sources. ) : ( value.map( (s: { readonly id: string; readonly scopeId: ScopeId; readonly name: string; readonly provider: string; }) => ( handleRemove(s)} /> ), ) )} ), })}
); }