configure mcp, refactors
This commit is contained in:
149
src/components/graph/ConfigNode.tsx
Normal file
149
src/components/graph/ConfigNode.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Handle, Position } from 'reactflow'
|
||||
import Editor, { loader } from '@monaco-editor/react'
|
||||
import FlowContext from '../../lib/flowContext'
|
||||
import {
|
||||
BaseNode,
|
||||
BaseNodeContent,
|
||||
BaseNodeFooter,
|
||||
BaseNodeHeader,
|
||||
BaseNodeHeaderTitle,
|
||||
} from './BaseNode'
|
||||
import { Pencil } from 'lucide-react'
|
||||
|
||||
// Ensure Monaco can load its web workers under Vite by pointing to a CDN
|
||||
loader.config({ paths: { vs: 'https://unpkg.com/monaco-editor@latest/min/vs' } })
|
||||
|
||||
type Props = {
|
||||
id: string
|
||||
data: any
|
||||
}
|
||||
|
||||
export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
|
||||
const [value, setValue] = useState<string>(data?.yaml ?? '# Enter YAML here\n')
|
||||
|
||||
const ctx = useContext(FlowContext)
|
||||
const setNodes = ctx?.setNodes
|
||||
const storedYaml = ctx?.nodes?.find((n: any) => n.id === id)?.data?.yaml ?? ''
|
||||
|
||||
const editorRef = useRef<any>(null)
|
||||
const monacoRef = useRef<any>(null)
|
||||
|
||||
const incomingEdges = ctx?.edges?.filter((e: any) => e.target === id) ?? []
|
||||
const incomingIds = incomingEdges.map((e: any) => e.source).sort()
|
||||
const connectedConfigNodes = (ctx?.nodes ?? []).filter((n: any) => incomingIds.includes(n.id) && n.type === 'config')
|
||||
|
||||
useEffect(() => {
|
||||
if (setNodes) {
|
||||
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, yaml: value } } : n)))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const onChange = useCallback(
|
||||
(val?: string) => {
|
||||
const v = val ?? ''
|
||||
setValue(v)
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('ConfigNode:onChange', id, v.substring(0, 60))
|
||||
if (setNodes) {
|
||||
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, yaml: v } } : n)))
|
||||
}
|
||||
},
|
||||
[id, setNodes]
|
||||
)
|
||||
|
||||
const handleEditorMount = useCallback((editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
monacoRef.current = monaco
|
||||
}, [])
|
||||
|
||||
const insertIncludeFromNode = useCallback(
|
||||
(sourceNode: any) => {
|
||||
const ref = `${sourceNode.id}.yaml`
|
||||
const includeText = `!include ${ref}\n`
|
||||
const editor = editorRef.current
|
||||
const monaco = monacoRef.current
|
||||
|
||||
try {
|
||||
if (editor && monaco) {
|
||||
const selection = editor.getSelection()
|
||||
let range
|
||||
if (selection) {
|
||||
range = new monaco.Range(selection.startLineNumber, selection.startColumn, selection.startLineNumber, selection.startColumn)
|
||||
} else {
|
||||
const model = editor.getModel()
|
||||
const lineCount = model.getLineCount()
|
||||
range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1)
|
||||
}
|
||||
editor.executeEdits('insert-include', [{ range, text: includeText, forceMoveMarkers: true }])
|
||||
const newVal = editor.getModel().getValue()
|
||||
onChange(newVal)
|
||||
return
|
||||
}
|
||||
|
||||
onChange(value + '\n' + includeText)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Insert include failed', err)
|
||||
}
|
||||
},
|
||||
[onChange, value]
|
||||
)
|
||||
|
||||
return (
|
||||
<BaseNode className="w-80">
|
||||
<BaseNodeHeader className="border-b">
|
||||
<Pencil className="size-4" />
|
||||
<BaseNodeHeaderTitle>{id}.yml</BaseNodeHeaderTitle>
|
||||
</BaseNodeHeader>
|
||||
|
||||
<BaseNodeContent>
|
||||
{connectedConfigNodes.length > 0 && (
|
||||
<div className="px-0 py-0 text-xs bg-gray-50 border-b border-gray-100 w-full">
|
||||
<div className="text-xs font-medium text-gray-600 px-3 pt-3">Connected configs</div>
|
||||
<div className="mt-1 space-y-1 px-3 pb-3">
|
||||
{connectedConfigNodes.map((n: any) => (
|
||||
<div key={n.id} className="flex items-center justify-between">
|
||||
<div className="text-xs text-gray-700">{n.data?.title ?? n.id}</div>
|
||||
<div>
|
||||
<button
|
||||
className="text-xs px-2 py-0.5 bg-white border rounded text-gray-600"
|
||||
onClick={() => insertIncludeFromNode(n)}
|
||||
>
|
||||
Insert include
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ height: 180 }} className="w-full">
|
||||
<Editor
|
||||
height="100%"
|
||||
defaultLanguage="yaml"
|
||||
value={value}
|
||||
theme="vs-light"
|
||||
onMount={handleEditorMount}
|
||||
onChange={onChange}
|
||||
options={{ minimap: { enabled: false }, fontSize: 12 }}
|
||||
/>
|
||||
</div>
|
||||
</BaseNodeContent>
|
||||
|
||||
<BaseNodeFooter>
|
||||
<div className="w-full text-xs text-gray-500">Stored YAML: {storedYaml ? `${storedYaml.length} chars` : 'none'}</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>
|
||||
)
|
||||
})
|
||||
|
||||
ConfigNode.displayName = 'ConfigNode'
|
||||
|
||||
export default ConfigNode
|
||||
Reference in New Issue
Block a user