import React, { useId, useState } from 'react' import { HardDrive } from 'lucide-react' import { STORAGE_BACKEND_TYPES, STORAGE_BACKEND_TYPE_IDS, type StorageBackend, type StorageBackendFieldDef, type StorageBackendTypeId, } from '@trek/shared' import { useTranslation } from '../../../i18n' import CustomSelect from '../../shared/CustomSelect' import { DialogButton, DialogFooter, DialogHeader, DialogSection, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs, } from '../../shared/DialogShell' import { EditorField, GRID_2, INPUT } from '../../shared/dialogParts' type FieldValues = Record function valuesOf(backend: StorageBackend | null): FieldValues { if (!backend) return {} const values: FieldValues = {} for (const [key, value] of Object.entries(backend.options)) { values[key] = Array.isArray(value) ? value : String(value) } return values } export interface BackendFormMirrorProps { /** Selectable replica targets (the form excludes its own name at render). */ candidates: string[] initialTargets: string[] } interface BackendFormProps { /** null = new backend */ initial: StorageBackend | null /** Every defined backend name — mirror-target options and the duplicate pre-check. */ backendNames: string[] /** * Present on non-mirror backends: renders the Mirror-targets composer * (replicas-on-primary — panel-supplied chrome, never a registry field). * When present, onCommit's second argument is always an array. */ mirror?: BackendFormMirrorProps onCommit: (backend: StorageBackend, mirrorTargets?: string[]) => void onCancel: () => void } /** A box of checkbox rows, the white list the settings cards use. */ const CHECK_ROWS = 'divide-y divide-edge-faint overflow-hidden rounded-[12px] border border-edge-faint bg-surface-card' const CHECK_ROW = 'flex cursor-pointer items-center gap-3 px-3.5 py-2.5 text-content hover:bg-surface-secondary' const CHECKBOX = 'h-4 w-4 flex-none cursor-pointer accent-accent' /** * Renders whatever STORAGE_BACKEND_TYPES declares, by field kind. The raw * `mirror` type is hidden from the type select — mirrors are composed via the * Mirror-targets prop block and synthesized by the panels (the ref-kind * renderers below stay for future registry types). Drawn as the planner's * editor dialog: head band, fields under eyebrows, the answers in the footer. */ export default function BackendForm({ initial, backendNames, mirror, onCommit, onCancel, }: BackendFormProps): React.ReactElement { const { t } = useTranslation() const labelId = useId() const fieldId = (key: string) => `${labelId}-${key}` const [type, setType] = useState(initial?.type ?? 'local') const [name, setName] = useState(initial?.name ?? '') const [values, setValues] = useState(() => valuesOf(initial)) const [targets, setTargets] = useState(mirror?.initialTargets ?? []) const fields = STORAGE_BACKEND_TYPES[type].fields as readonly StorageBackendFieldDef[] const refOptions = backendNames.filter((candidate) => candidate !== name.trim()) const setValue = (key: string, value: string | string[]) => setValues((prev) => ({ ...prev, [key]: value })) const filled = (field: StorageBackendFieldDef): boolean => { const value = values[field.key] if (field.kind === 'backend-ref-list') return Array.isArray(value) && value.length > 0 return typeof value === 'string' && value.trim() !== '' } const duplicate = name.trim() !== (initial?.name ?? '') && backendNames.includes(name.trim()) const canApply = name.trim() !== '' && !duplicate && fields.every((f) => !f.required || filled(f)) const apply = () => { const options: Record = {} for (const field of fields) { const value = values[field.key] if (field.kind === 'backend-ref-list') { options[field.key] = Array.isArray(value) ? value : [] continue } const text = typeof value === 'string' ? value : '' if (text === '' && !field.required) continue // omitted → the shared schema default applies options[field.key] = field.kind === 'number' ? Number(text) : text } // The options were built from the same field defs the schema is generated // from — this is the same sanctioned cast storageModel.asWireBackend makes. const payload = { name: name.trim(), type, options } as StorageBackend // Arity matters: the landed tests pin single-argument calls when no mirror // block is supplied (toHaveBeenCalledWith treats a trailing undefined as a // mismatch), so the second argument exists only when the composer does. if (mirror) onCommit(payload, targets) else onCommit(payload) } const renderField = (field: StorageBackendFieldDef): React.ReactElement => { const value = values[field.key] if (field.kind === 'backend-ref') { return ( setValue(field.key, String(next))} options={refOptions.map((candidate) => ({ value: candidate, label: candidate }))} placeholder={t(field.labelKey)} size="sm" /> ) } if (field.kind === 'backend-ref-list') { const selected = Array.isArray(value) ? value : [] return (
{refOptions.map((candidate) => ( ))}
) } const inputType = field.kind === 'secret' ? 'password' : field.kind === 'number' ? 'number' : 'text' return ( setValue(field.key, e.target.value)} placeholder={field.defaultValue !== undefined ? String(field.defaultValue) : ''} spellCheck={false} autoComplete="off" className={`${INPUT} ${inputType === 'number' ? 'tabular-nums' : ''}`} /> ) } const visibleCandidates = mirror ? mirror.candidates.filter((candidate) => candidate !== name.trim()) : [] return ( } tint={NEUTRAL_TINT} labelId={labelId} onClose={onCancel} title={initial ? t('storage.form.editTitle') : t('storage.form.addTitle')} /> } footer={ {t('storage.form.cancel')} {t('storage.form.apply')} } >
setName(e.target.value)} spellCheck={false} autoComplete="off" className={`${INPUT} font-geist`} /> {duplicate && (

{t('storage.form.duplicateName', { name: name.trim() })}

)}
{ setType(next as StorageBackendTypeId) setValues({}) // a different type has different fields }} options={STORAGE_BACKEND_TYPE_IDS.filter((id) => id !== 'mirror').map((id) => ({ value: id, label: t(`storage.type.${id}`), }))} size="sm" disabled={initial !== null} />
{fields.length > 0 &&
{fields.map(renderField)}
} {mirror && (

{t('storage.mirror.targetsHelp')}

{visibleCandidates.length > 0 && (
{visibleCandidates.map((candidate) => ( ))}
)} {targets.length > 0 && (

{t('storage.mirror.latencyNote')}

)}
)}
) }