import { useState, type ReactNode } from "react"; import { PlusIcon } from "lucide-react"; import type { ScopeId } from "@executor-js/sdk/shared"; import { Button } from "../components/button"; import { CardStack, CardStackContent, CardStackEmpty, CardStackEntry, } from "../components/card-stack"; import { Field, FieldGroup, FieldLabel } from "../components/field"; import { Input } from "../components/input"; import { defaultHeaderAuthPresets, type HeaderAuthPreset, type HeaderState, SecretHeaderAuthRow, type SecretCredentialPreviewComponent, type SecretCredentialRowCopy, } from "./secret-header-auth"; import type { CredentialTargetScopeOption } from "./credential-target-scope"; import type { SecretPickerSecret } from "./secret-picker"; export interface HeadersListProps { readonly headers: readonly HeaderState[]; readonly onHeadersChange: (headers: HeaderState[]) => void; readonly existingSecrets?: readonly SecretPickerSecret[]; /** Presets offered in the quick-add picker. Defaults to `defaultHeaderAuthPresets`. */ readonly presets?: readonly HeaderAuthPreset[]; /** When true, only allow a single header (hide add button, disable remove). */ readonly singleHeader?: boolean; /** Text shown in the empty state. */ readonly emptyLabel?: ReactNode; readonly addLabel?: ReactNode; readonly addAriaLabel?: string; readonly rowCopy?: Partial; readonly rowPreviewComponent?: SecretCredentialPreviewComponent; /** * Display name of the source that owns these headers (e.g. "Axiom"). Used * to derive unique default secret labels/IDs like `axiom-authorization`. */ readonly sourceName?: string; /** Inline-created secrets are written to this explicit scope. */ readonly targetScope: ScopeId; /** Scope choices available for where this source credential is used. */ readonly credentialScopeOptions?: readonly CredentialTargetScopeOption[]; /** Scope choices for where this source credential is used. */ readonly bindingScopeOptions?: readonly CredentialTargetScopeOption[]; readonly defaultValueKind?: HeaderState["valueKind"]; } export function HeadersList({ headers, onHeadersChange, existingSecrets = [], presets = defaultHeaderAuthPresets, singleHeader = false, emptyLabel = "No headers", addLabel, addAriaLabel = "Add header", rowCopy, rowPreviewComponent, sourceName, targetScope, bindingScopeOptions, defaultValueKind = "secret", }: HeadersListProps) { const [picking, setPicking] = useState(false); const canAddMore = !singleHeader || headers.length === 0; const addFirstPreset = () => { const preset = presets[0]; if (presets.length === 1 && preset) { addHeaderFromPreset(preset); return; } setPicking(true); }; const addHeaderFromPreset = (preset: HeaderAuthPreset) => { onHeadersChange([ ...headers, { name: preset.name, prefix: preset.prefix, presetKey: preset.key, secretId: null, valueKind: preset.valueKind ?? defaultValueKind, targetScope, }, ]); setPicking(false); }; const updateHeader = ( index: number, update: Partial<{ name: string; secretId: string | null; prefix?: string; presetKey?: string; valueKind?: HeaderState["valueKind"]; literalValue?: string; targetScope?: ScopeId; secretScope?: ScopeId; }>, ) => { onHeadersChange(headers.map((entry, i) => (i === index ? { ...entry, ...update } : entry))); }; const removeHeader = (index: number) => { onHeadersChange(headers.filter((_, i) => i !== index)); }; return ( {picking ? ( setPicking(false)} /> ) : headers.length === 0 ? ( canAddMore ? ( {emptyLabel}} onClick={addFirstPreset} ariaLabel={addAriaLabel} /> ) : ( {emptyLabel} ) ) : ( <> {headers.map((header, index) => ( updateHeader(index, update)} onSelectSecret={(secretId, scopeId) => updateHeader(index, { secretId, ...(scopeId ? { secretScope: scopeId } : {}), }) } onRemove={singleHeader ? undefined : () => removeHeader(index)} existingSecrets={existingSecrets} sourceName={sourceName} bindingScopeOptions={bindingScopeOptions} copy={rowCopy} previewComponent={rowPreviewComponent} /> ))} {canAddMore && ( )} )} ); } function HeaderRow(props: { readonly header: HeaderState; readonly targetScope: ScopeId; readonly onChange: ( update: Partial<{ name: string; secretId: string | null; prefix?: string; presetKey?: string; valueKind?: HeaderState["valueKind"]; literalValue?: string; targetScope?: ScopeId; secretScope?: ScopeId; }>, ) => void; readonly onSelectSecret: (secretId: string, scopeId?: ScopeId) => void; readonly onRemove?: () => void; readonly existingSecrets: readonly SecretPickerSecret[]; readonly sourceName?: string; readonly bindingScopeOptions?: readonly CredentialTargetScopeOption[]; readonly copy?: Partial; readonly previewComponent?: SecretCredentialPreviewComponent; }) { if (props.header.valueKind === "text") { return ( props.onChange(update)} onRemove={props.onRemove} rowLabel={props.copy?.rowLabel} nameLabel={props.copy?.nameLabel} namePlaceholder={props.copy?.namePlaceholder} /> ); } return ( ); } function TextHeaderRow(props: { readonly name: string; readonly value: string; readonly onChange: (update: { name?: string; literalValue?: string }) => void; readonly onRemove?: () => void; readonly rowLabel?: string; readonly nameLabel?: string; readonly namePlaceholder?: string; }) { return (
{props.rowLabel ?? "Header"} {props.onRemove && ( )}
{props.nameLabel ?? "Name"} props.onChange({ name: (event.target as HTMLInputElement).value })} placeholder={props.namePlaceholder ?? "X-Organization-Id"} className="font-mono" /> Value props.onChange({ literalValue: (event.target as HTMLInputElement).value }) } placeholder="workspace-id" className="font-mono" />
); } interface AddHeaderRowProps { readonly onClick: () => void; readonly leading?: ReactNode; readonly ariaLabel: string; } function AddHeaderRow({ onClick, leading, ariaLabel }: AddHeaderRowProps) { return ( // oxlint-disable-next-line react/forbid-elements ); } interface HeaderPresetPickerProps { readonly presets: readonly HeaderAuthPreset[]; readonly onPick: (preset: HeaderAuthPreset) => void; readonly onCancel: () => void; } function HeaderPresetPicker({ presets, onPick, onCancel }: HeaderPresetPickerProps) { return ( {presets.map((preset) => ( ))} ); }