109 lines
4.8 KiB
TypeScript
109 lines
4.8 KiB
TypeScript
import React from 'react'
|
|
import { Handle, Position } from 'reactflow'
|
|
import yaml from 'js-yaml'
|
|
import FlowContext from '../lib/flowContext'
|
|
|
|
type Props = {
|
|
id: string
|
|
data: any
|
|
}
|
|
|
|
export default function RenderingNode({ id }: Props) {
|
|
const [output, setOutput] = React.useState<string>('')
|
|
|
|
const ctx = React.useContext(FlowContext)
|
|
const nodes = ctx?.nodes ?? []
|
|
const edges = ctx?.edges ?? []
|
|
|
|
const incomingEdges = edges.filter((e: any) => e.target === id)
|
|
const incomingIds = incomingEdges.map((e: any) => e.source).sort()
|
|
const srcId = incomingIds.length > 0 ? incomingIds[0] : null
|
|
const srcNode = nodes.find((n: any) => n.id === srcId)
|
|
const yamlText = srcNode && srcNode.type === 'config' ? srcNode?.data?.yaml ?? '' : ''
|
|
|
|
React.useEffect(() => {
|
|
// debug
|
|
// eslint-disable-next-line no-console
|
|
console.debug('RenderingNode:selection', { id, incomingIds, srcNode: srcNode ? { id: srcNode.id, type: srcNode.type, yaml: (srcNode.data?.yaml ?? '').slice(0, 60) } : null })
|
|
|
|
if (!yamlText) {
|
|
if (incomingIds.length === 0) {
|
|
setOutput(`No configuration connected (nodes: ${nodes.length}, edges: ${edges.length})`)
|
|
return
|
|
}
|
|
setOutput('No YAML found on connected configuration node')
|
|
return
|
|
}
|
|
|
|
try {
|
|
// resolve !include directives by inlining YAML from reachable config nodes
|
|
const resolveIncludes = (text: string, visited = new Set<string>()): string => {
|
|
const includeRegex = /^(\s*)!include\s+([^\s]+)\s*$/gm
|
|
return text.replace(includeRegex, (match, indent, ref) => {
|
|
const refName = ref.endsWith('.yaml') ? ref.slice(0, -5) : ref
|
|
// try to find a node by id or by title
|
|
const refNode = nodes.find((n: any) => n.id === refName || (n.data?.title === refName))
|
|
if (!refNode) {
|
|
throw new Error(`Included node not found: ${ref}`)
|
|
}
|
|
if (visited.has(refNode.id)) {
|
|
throw new Error(`Circular include detected: ${ref}`)
|
|
}
|
|
|
|
// ensure refNode can reach this rendering node
|
|
const isReachable = (startId: string, targetId: string) => {
|
|
const q: string[] = [startId]
|
|
const seen = new Set<string>([startId])
|
|
while (q.length) {
|
|
const cur = q.shift()!
|
|
if (cur === targetId) return true
|
|
for (const e of edges) {
|
|
if (e.source === cur && !seen.has(e.target)) {
|
|
seen.add(e.target)
|
|
q.push(e.target)
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
if (!isReachable(refNode.id, id)) {
|
|
throw new Error(`Included node not connected to renderer: ${ref}`)
|
|
}
|
|
|
|
visited.add(refNode.id)
|
|
const includedRaw = String(refNode.data?.yaml ?? '')
|
|
const resolved = resolveIncludes(includedRaw, visited)
|
|
visited.delete(refNode.id)
|
|
|
|
// indent included content to match include position
|
|
const indented = resolved
|
|
.split('\n')
|
|
.map((line: string, idx: number) => (line === '' ? '' : indent + line))
|
|
.join('\n')
|
|
return indented
|
|
})
|
|
}
|
|
|
|
const resolvedYaml = resolveIncludes(yamlText)
|
|
const parsed = yaml.load(resolvedYaml)
|
|
const newOutput = JSON.stringify(parsed, null, 2)
|
|
setOutput((prev) => (prev === newOutput ? prev : newOutput))
|
|
} catch (err: any) {
|
|
const msg = 'YAML parse/include error: ' + err.message
|
|
setOutput((prev) => (prev === msg ? prev : msg))
|
|
}
|
|
}, [id, incomingIds.join(','), yamlText, nodes.length, edges.length])
|
|
|
|
return (
|
|
<div className="w-80 bg-white rounded shadow hover:shadow-md border border-gray-200">
|
|
<div className="px-3 py-2 border-b border-gray-100 flex items-center justify-between">
|
|
<div className="text-sm font-medium">{id}</div>
|
|
<div className="text-xs text-gray-400">RENDERER</div>
|
|
</div>
|
|
<pre className="text-xs text-gray-800 max-h-40 overflow-auto p-3">{output}</pre>
|
|
<Handle type="target" position={Position.Left} id="in" style={{ background: '#F97316' }} />
|
|
</div>
|
|
)
|
|
}
|