From 8e5147cfd1d9b48463e499ae19bafa286b943eba Mon Sep 17 00:00:00 2001 From: dtoro Date: Thu, 5 Mar 2026 23:46:12 +0100 Subject: [PATCH] add basic condfig and rendering --- package-lock.json | 70 ++++++++++++++++++++++++++++++++ package.json | 17 ++++---- src/App.tsx | 61 ++++++++++++++++------------ src/components/ConfigNode.tsx | 68 +++++++++++++++++++++++++++++++ src/components/CustomNode.tsx | 36 +++++++++------- src/components/RenderingNode.tsx | 55 +++++++++++++++++++++++++ src/lib/flowContext.tsx | 12 ++++++ src/styles.css | 14 +++++++ 8 files changed, 285 insertions(+), 48 deletions(-) create mode 100644 src/components/ConfigNode.tsx create mode 100644 src/components/RenderingNode.tsx create mode 100644 src/lib/flowContext.tsx diff --git a/package-lock.json b/package-lock.json index 2725a58..392dfcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "reactflow-shadcn-app", "version": "0.1.0", "dependencies": { + "@monaco-editor/react": "^4.7.0", + "js-yaml": "^4.1.0", + "monaco-editor": "^0.55.1", "react": "18.2.0", "react-dom": "18.2.0", "reactflow": "^11.0.0", @@ -1193,6 +1196,29 @@ } } }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.41.3", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", @@ -1730,6 +1756,13 @@ "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/validate-npm-package-name": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", @@ -2703,6 +2736,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -3921,6 +3963,18 @@ "node": ">=12" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4061,6 +4115,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5320,6 +5384,12 @@ "node": ">=0.10.0" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index 55673e8..b54f6c1 100644 --- a/package.json +++ b/package.json @@ -8,19 +8,22 @@ "preview": "vite preview" }, "dependencies": { + "@monaco-editor/react": "^4.7.0", + "js-yaml": "^4.1.0", + "monaco-editor": "^0.55.1", "react": "18.2.0", "react-dom": "18.2.0", "reactflow": "^11.0.0", "shadcn": "^3.8.5" }, "devDependencies": { - "vite": "^4.0.0", - "@vitejs/plugin-react": "^3.0.0", - "tailwindcss": "^3.4.0", - "postcss": "^8.4.0", - "autoprefixer": "^10.4.0", - "typescript": "^5.0.0", "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0" + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "^3.0.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "typescript": "^5.0.0", + "vite": "^4.0.0" } } diff --git a/src/App.tsx b/src/App.tsx index 8a4ffa1..bd0ee42 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,31 +15,38 @@ import ReactFlow, { EdgeChange, } from 'reactflow' import CustomNode from './components/CustomNode' +import ConfigNode from './components/ConfigNode' +import RenderingNode from './components/RenderingNode' +import FlowContext from './lib/flowContext' + +// Generate short unique node ids when missing +const genId = () => `node_${Math.random().toString(36).slice(2, 9)}` const initialNodes: Node[] = [ { - id: '1', - position: { x: 250, y: 5 }, - data: { label: 'Node A' }, - type: 'custom', + id: 'config-1', + position: { x: 50, y: 50 }, + data: { yaml: "# example:\nmessage: Hello from config" }, + type: 'config', }, { - id: '2', - position: { x: 100, y: 200 }, - data: { label: 'Node B' }, - type: 'custom', + id: 'render-1', + position: { x: 350, y: 80 }, + data: {}, + type: 'render', }, -] +].map((n) => ({ ...n, id: n.id ?? genId() })) -const initialEdges: Edge[] = [ - { id: 'e1-2', source: '1', target: '2' }, -] +const initialEdges: Edge[] = [{ id: 'e-config-render', source: 'config-1', target: 'render-1' }] export default function App() { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) - const nodeTypes = React.useMemo(() => ({ custom: CustomNode }), []) + const nodeTypes = React.useMemo( + () => ({ custom: CustomNode, config: ConfigNode, render: RenderingNode }), + [] + ) const onConnect = React.useCallback( (params: Connection) => setEdges((eds) => addEdge(params, eds)), @@ -48,19 +55,21 @@ export default function App() { return (
- - - - - + + + + + + +
) } diff --git a/src/components/ConfigNode.tsx b/src/components/ConfigNode.tsx new file mode 100644 index 0000000..bd5ee93 --- /dev/null +++ b/src/components/ConfigNode.tsx @@ -0,0 +1,68 @@ +import React from 'react' +import { Handle, Position } from 'reactflow' +import Editor, { loader } from '@monaco-editor/react' +import FlowContext from '../lib/flowContext' + +// Ensure Monaco can load its web workers under Vite by pointing to a CDN +// This avoids the editor hanging while trying to locate worker scripts locally. +loader.config({ paths: { vs: 'https://unpkg.com/monaco-editor@latest/min/vs' } }) + +type Props = { + id: string + data: any +} + +export default function ConfigNode({ id, data }: Props) { + const [value, setValue] = React.useState(data?.yaml ?? "# Enter YAML here\n") + const ctx = React.useContext(FlowContext) + const setNodes = ctx?.setNodes + const storedYaml = ctx?.nodes?.find((n: any) => n.id === id)?.data?.yaml ?? '' + + React.useEffect(() => { + // keep node data in sync on mount + 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 = React.useCallback( + (val?: string) => { + const v = val ?? '' + setValue(v) + // debug log to help trace updates + // 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] + ) + + return ( +
+
+
Configuration
+
YAML
+
+ +
+ +
+
+ Stored YAML: {storedYaml ? `${storedYaml.length} chars` : 'none'} +
+ + + +
+ ) +} diff --git a/src/components/CustomNode.tsx b/src/components/CustomNode.tsx index 696ca6b..8f9015f 100644 --- a/src/components/CustomNode.tsx +++ b/src/components/CustomNode.tsx @@ -3,23 +3,29 @@ import { Handle, Position } from 'reactflow' export default function CustomNode({ data }: any) { return ( -
-
{data.label}
-
Custom content
+
+
+
{data.label}
+
ID: {data?.id ?? ''}
+
- +
Custom content
- +
+ + + +
) } diff --git a/src/components/RenderingNode.tsx b/src/components/RenderingNode.tsx new file mode 100644 index 0000000..9146d54 --- /dev/null +++ b/src/components/RenderingNode.tsx @@ -0,0 +1,55 @@ +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 { + const parsed = yaml.load(yamlText) + const newOutput = JSON.stringify(parsed, null, 2) + setOutput((prev) => (prev === newOutput ? prev : newOutput)) + } catch (err: any) { + const msg = 'YAML parse error: ' + err.message + setOutput((prev) => (prev === msg ? prev : msg)) + } + }, [id, incomingIds.join(','), yamlText, nodes.length, edges.length]) + + return ( +
+
Rendering
+
{output}
+ +
+ ) +} diff --git a/src/lib/flowContext.tsx b/src/lib/flowContext.tsx new file mode 100644 index 0000000..4bc0592 --- /dev/null +++ b/src/lib/flowContext.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +export type FlowContextValue = { + nodes: any[] + setNodes: (updater: any) => void + edges: any[] + setEdges: (updater: any) => void +} + +const FlowContext = React.createContext(null) + +export default FlowContext diff --git a/src/styles.css b/src/styles.css index 0ef756b..c98c4d8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -17,4 +17,18 @@ body { .reactflow-wrapper { width: 100%; height: 100vh; +} + +/* Node specific overrides */ +.react-flow__node { + display: block; +} + +.react-flow__node .react-flow__handle { + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.06); +} + +/* Small helper for monospace pre output */ +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace; } \ No newline at end of file