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 | 137x 137x 44x 32x 32x 42x 32x 30x 30x 30x 6x 24x 9x 1298x 19x 14x 1329x 54x 11x 11x 11x 54x 8x 1180x 46x 137x 218x 218x 218x 8x 8x 8x 218x 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>
)
}
|