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 | 145x 145x 39x 27x 27x 42x 27x 30x 30x 25x 6x 24x 9x 1012x 19x 14x 1043x 54x 11x 11x 11x 54x 8x 920x 46x 145x 184x 184x 184x 7x 7x 7x 184x 44x 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>
)
}
|