configure mcp, refactors

This commit is contained in:
2026-03-06 20:43:43 +01:00
parent b526f99982
commit 7b08db2791
7 changed files with 1023 additions and 210 deletions

View File

@@ -0,0 +1,171 @@
import { memo, useContext, useEffect, useMemo, useState } from 'react'
import { Handle, Position } from 'reactflow'
import yaml from 'js-yaml'
import FlowContext from '../../lib/flowContext'
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia } from '../ui/empty'
import {
BaseNode,
BaseNodeContent,
BaseNodeFooter,
BaseNodeHeader,
BaseNodeHeaderTitle,
} from './BaseNode'
import { Rocket } from 'lucide-react'
type Props = {
id: string
data?: any
}
export const RenderingNode = memo(function RenderingNode({ id }: Props) {
const [output, setOutput] = useState<string>('')
const [error, setError] = useState<null | { kind: string; message: string }>(null)
const ctx = useContext(FlowContext)
const nodes = ctx?.nodes ?? []
const edges = ctx?.edges ?? []
const setNodes = ctx?.setNodes
const setEdges = ctx?.setEdges
const incomingEdges = useMemo(() => edges.filter((e: any) => e.target === id), [edges, id])
const incomingIds = useMemo(() => incomingEdges.map((e: any) => e.source).sort(), [incomingEdges])
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 ?? '' : ''
const srcData = srcNode?.data ?? {}
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('')
setError(null)
return
}
setOutput('')
setError({ kind: 'no-yaml', message: 'No YAML found on connected configuration node' })
return
}
try {
const resolveIncludes = (text: string, visited = new Set<string>()): string => {
const includeRegex = /^(\s*)!include\s+([^\s]+)\s*$/gm
return text.replace(includeRegex, (match: string, indent: string, ref: string) => {
const refName = ref.endsWith('.yaml') ? ref.slice(0, -5) : ref
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}`)
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)
const indented = resolved
.split('\n')
.map((line: string) => (line === '' ? '' : indent + line))
.join('\n')
return indented
})
}
const resolvedYaml = resolveIncludes(yamlText)
const parsed = yaml.load(resolvedYaml)
const newOutput = JSON.stringify(parsed, null, 2)
setError(null)
setOutput((prev) => (prev === newOutput ? prev : newOutput))
} catch (err: any) {
const msg = 'YAML parse/include error: ' + err.message
setOutput('')
setError({ kind: 'parse', message: msg })
}
}, [id, incomingIds.join(','), yamlText, nodes.length, edges.length])
return (
<BaseNode className="w-96">
<BaseNodeHeader className="border-b">
<Rocket className="size-4" />
<BaseNodeHeaderTitle>{id}</BaseNodeHeaderTitle>
</BaseNodeHeader>
<BaseNodeContent>
{incomingIds.length === 0 ? (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<Rocket className="size-6" />
</EmptyMedia>
<EmptyTitle>No configuration connected</EmptyTitle>
<EmptyDescription>Connect a Configuration node or create one. The renderer will display parsed YAML.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<button
className="inline-flex items-center rounded bg-primary px-3 py-1 text-xs text-primary-foreground"
onClick={() => {
if (!setNodes || !setEdges) return
const genId = () => `node_${Math.random().toString(36).slice(2, 9)}`
const nid = genId()
const thisNode = nodes.find((n: any) => n.id === id)
const pos = thisNode?.position ?? { x: 0, y: 0 }
const newPos = { x: pos.x - 220, y: pos.y }
const newNode = { id: nid, type: 'config', position: newPos, data: { yaml: '# Enter YAML\n', title: `config-${nid}` } }
setNodes((nds: any[]) => nds.concat(newNode))
const edgeId = `e-${nid}-${id}`
setEdges((eds: any[]) => eds.concat({ id: edgeId, source: nid, target: id }))
}}
>
Create Config
</button>
</EmptyContent>
</Empty>
) : error ? (
srcData?.renderError ? (
srcData.renderError(error)
) : srcData?.errorHtml ? (
<div className="p-3 text-xs text-red-700" dangerouslySetInnerHTML={{ __html: String(srcData.errorHtml) }} />
) : (
<div className="p-3 text-xs text-red-700">{error.message}</div>
)
) : (
<pre className="text-xs text-gray-800 max-h-48 overflow-auto">{output}</pre>
)}
</BaseNodeContent>
<BaseNodeFooter>
<div className="w-full text-xs text-muted-foreground">Parsed YAML output</div>
</BaseNodeFooter>
<Handle type="target" position={Position.Left} id="ain" style={{ background: '#F97316', top: 20, width: 12, height: 12, borderRadius: 3 }} />
<Handle type="source" position={Position.Right} id="out" style={{ background: '#10B981', top: 20, width: 12, height: 12, borderRadius: 3 }} />
</BaseNode>
)
})
RenderingNode.displayName = 'RenderingNode'
export default RenderingNode