Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 134x 134x 44x 32x 32x 42x 32x 30x 30x 30x 6x 24x 9x 1276x 19x 14x 1307x 54x 11x 11x 11x 54x 8x 1160x 46x 134x 216x 216x 216x 8x 8x 8x 216x 52x 5x | import { useState, useCallback } from 'react'
import { load } from 'js-yaml'
import type { LintResult, ConfigSchemaHelpNode } from '@/layers/interfaces'
interface YamlEditorProps {
value: string
onChange: (value: string) => void
readOnly?: boolean
lint?: (parsed: unknown, raw: string) => LintResult
help?: ConfigSchemaHelpNode[]
'data-testid'?: string
}
interface LintState {
syntaxError: string | null
lintResult: LintResult | null
}
function evaluate(raw: string, lint?: YamlEditorProps['lint']): LintState {
if (raw.trim() === '') return { syntaxError: null, lintResult: null }
let parsed: unknown
try {
parsed = load(raw)
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err)
return { syntaxError: `Invalid YAML: ${message}`, lintResult: null }
}
if (!lint) return { syntaxError: null, lintResult: null }
return { syntaxError: null, lintResult: lint(parsed, raw) }
}
function HelpNode({ node, depth }: { node: ConfigSchemaHelpNode; depth: number }) {
return (
<div className={depth > 0 ? 'ml-3 flex flex-col gap-0.5' : 'flex flex-col gap-0.5'} data-testid={`yaml-help-node-${node.key}`}>
<span>
<span className="font-mono font-medium">{node.key}</span>
{node.required && <span className="ml-1 text-muted-foreground">(required)</span>}
{node.description && <span className="ml-1 text-muted-foreground">{node.description}</span>}
</span>
{node.children && node.children.length > 0 && (
<div className="flex flex-col gap-0.5">
{node.children.map((child) => (
<HelpNode key={child.key} node={child} depth={depth + 1} />
))}
</div>
)}
</div>
)
}
export function YamlEditor({ value, onChange, readOnly = false, lint, help, 'data-testid': dataTestId }: YamlEditorProps) {
const [state, setState] = useState<LintState>(() => evaluate(value, lint))
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const raw = e.target.value
onChange(raw)
setState(evaluate(raw, lint))
},
[onChange, lint],
)
if (readOnly) {
return (
<div
data-testid={dataTestId}
className="font-mono text-sm whitespace-pre-wrap rounded-md border border-input bg-muted/30 px-3 py-2 min-h-[5rem] text-muted-foreground"
>
{value || <span className="italic">—</span>}
</div>
)
}
return (
<div className="flex flex-col gap-1">
<textarea
value={value}
onChange={handleChange}
data-testid={dataTestId}
className="flex min-h-[12rem] w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
spellCheck={false}
/>
{state.syntaxError && (
<p className="text-xs text-destructive" data-testid="yaml-error">
{state.syntaxError}
</p>
)}
{state.lintResult?.errors.map((issue, index) => (
<p key={`lint-error-${index}`} className="text-xs text-destructive" data-testid="yaml-lint-error">
{issue.message}
</p>
))}
{state.lintResult?.warnings.map((issue, index) => (
<p key={`lint-warning-${index}`} className="text-xs text-amber-600 dark:text-amber-400" data-testid="yaml-lint-warning">
{issue.message}
</p>
))}
{help && help.length > 0 && (
<div data-testid="yaml-help" className="mt-1 flex flex-col gap-0.5 rounded-md border border-input bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
<p className="font-medium text-foreground">Config keys</p>
{help.map((node) => (
<HelpNode key={node.key} node={node} depth={0} />
))}
</div>
)}
</div>
)
}
|