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('') 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 => { 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([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 (
{id}
RENDERER
{output}
) }