218 lines
6.0 KiB
TypeScript
218 lines
6.0 KiB
TypeScript
import { useState, useCallback, ReactNode } from 'react';
|
||
import {
|
||
ReactFlow,
|
||
applyNodeChanges,
|
||
applyEdgeChanges,
|
||
addEdge,
|
||
Background,
|
||
Controls,
|
||
ReactFlowProvider,
|
||
NodeTypes,
|
||
NodeChange,
|
||
EdgeChange,
|
||
MarkerType,
|
||
NodeMouseHandler,
|
||
OnConnect,
|
||
MiniMap,
|
||
IsValidConnection,
|
||
EdgeMouseHandler
|
||
} from '@xyflow/react';
|
||
import '@xyflow/react/dist/style.css';
|
||
import { AppNode, AppEdge, NodeData, EdgeData, Settings } from './types/graph';
|
||
import CustomNode from './components/CustomNode';
|
||
import NodeEditor from './components/NodeEditor';
|
||
import EdgeEditor from './components/EdgeEditor'
|
||
import Toggle from "./components/Toggle"
|
||
import { generateWireGuardPrivateKey } from './utils/wireguardConfig';
|
||
import './App.css';
|
||
|
||
const initialNodes: AppNode[] = [];
|
||
const initialEdges: AppEdge[] = [];
|
||
const initialSettings : Settings = {
|
||
listenPort: 38894,
|
||
mtu: 1420,
|
||
};
|
||
|
||
const nodeTypes : NodeTypes = {
|
||
custom: CustomNode,
|
||
};
|
||
|
||
function generateNodeData(count: number) : NodeData | null {
|
||
const privateKey = generateWireGuardPrivateKey();
|
||
const node : NodeData = {
|
||
id: `n-${crypto.randomUUID()}`,
|
||
label: `Node-${count + 1}`,
|
||
privateKey: privateKey
|
||
}
|
||
return node
|
||
}
|
||
|
||
function FlowContent(): ReactNode {
|
||
const [nodes, setNodes] = useState<AppNode[]>(initialNodes);
|
||
const [edges, setEdges] = useState<AppEdge[]>(initialEdges);
|
||
const [settings, setSettings] = useState<Settings>(initialSettings);
|
||
|
||
const [editingNode, setEditingNode] = useState<NodeData | undefined>(undefined);
|
||
const [editingEdge, setEditingEdge] = useState<EdgeData | undefined>(undefined);
|
||
const [enableTwoWay, setEnableTwoWay] = useState(false);
|
||
|
||
const onNodesChange = useCallback(
|
||
(changes: NodeChange<AppNode>[]) => setNodes((nds) => applyNodeChanges<AppNode>(changes, nds)),
|
||
[]);
|
||
|
||
const onEdgesChange = useCallback(
|
||
(changes : EdgeChange<AppEdge>[]) => setEdges((eds) => applyEdgeChanges<AppEdge>(changes, eds)),
|
||
[]);
|
||
|
||
const onConnect = useCallback<OnConnect>(
|
||
(params) => {
|
||
const id = `e-${crypto.randomUUID()}`
|
||
const newEdge : AppEdge = {
|
||
...params,
|
||
id: id,
|
||
animated: !enableTwoWay,
|
||
markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed },
|
||
data : {
|
||
id: id,
|
||
isTwoWayEdge: enableTwoWay
|
||
}
|
||
}
|
||
return setEdges((eds) => addEdge<AppEdge>(newEdge, eds));
|
||
},
|
||
[enableTwoWay]);
|
||
|
||
const onNodeClick = useCallback<NodeMouseHandler<AppNode>>(
|
||
(_event, node) => setEditingNode(node.data),
|
||
[]);
|
||
|
||
const onEdgeClick = useCallback<EdgeMouseHandler<AppEdge>>(
|
||
(_event, edge) => setEditingEdge(edge.data),
|
||
[]
|
||
)
|
||
|
||
const validateConnection = useCallback<IsValidConnection>(
|
||
(connection) => {
|
||
if (connection.source === connection.target) {
|
||
return false;
|
||
}
|
||
const isDuplicate = edges.some(
|
||
(edge) => (
|
||
(edge.source === connection.source && edge.target === connection.target) ||
|
||
(edge.source === connection.target && edge.target === connection.source)
|
||
)
|
||
);
|
||
return !isDuplicate;
|
||
},
|
||
[edges]);
|
||
|
||
const handleAddNode = (): void => {
|
||
const result = generateNodeData(nodes.length);
|
||
if(result == null) return;
|
||
const newNode: AppNode = {
|
||
id: result.id,
|
||
position: { x: 0, y: 0 },
|
||
data: result,
|
||
type: 'custom',
|
||
selected: true
|
||
};
|
||
setNodes((prev) => [...prev.map(node => ({ ...node, selected: false })), newNode]);
|
||
};
|
||
|
||
const handleUpdateNode = (updatedData: NodeData): void => {
|
||
setNodes((prev) =>
|
||
prev.map((node) => {
|
||
if (node.data.id === editingNode?.id) {
|
||
return { ...node, data: updatedData };
|
||
}
|
||
return node;
|
||
})
|
||
);
|
||
setEditingNode(undefined);
|
||
};
|
||
|
||
const handleUpdateEdge = (updatedData: EdgeData): void => {
|
||
setEdges((prev) =>
|
||
prev.map((edge) => {
|
||
if (edge.data && edge.data.id === editingEdge?.id) {
|
||
return { ...edge, data: updatedData };
|
||
}
|
||
return edge;
|
||
})
|
||
);
|
||
setEditingNode(undefined);
|
||
};
|
||
|
||
return (
|
||
<div style={{ width: '100vw', height: '100vh' }}>
|
||
<ReactFlow
|
||
nodes={nodes}
|
||
edges={edges}
|
||
onNodesChange={onNodesChange}
|
||
onEdgesChange={onEdgesChange}
|
||
onConnect={onConnect}
|
||
onNodeDoubleClick={onNodeClick}
|
||
onEdgeDoubleClick={onEdgeClick}
|
||
nodeTypes={nodeTypes}
|
||
deleteKeyCode={["Delete"]}
|
||
fitView
|
||
isValidConnection={validateConnection}
|
||
>
|
||
<Background />
|
||
<Controls />
|
||
<MiniMap
|
||
nodeColor={(n) => {
|
||
if (n.type === 'input') return 'blue';
|
||
return '#eee';
|
||
}}
|
||
maskColor="rgba(0, 0, 0, 0.1)"
|
||
position="bottom-right" // 也可以是 top-right 等
|
||
/>
|
||
</ReactFlow>
|
||
|
||
<div className="toolbar">
|
||
<div className="toolbar-group">
|
||
<Toggle
|
||
className="toolbar-item"
|
||
checked = {enableTwoWay}
|
||
onChange={checked => setEnableTwoWay(checked)} >双向连接</Toggle>
|
||
|
||
<button className="toolbar-btn" onClick={handleAddNode} title="添加新节点">
|
||
➕ 添加节点
|
||
</button>
|
||
|
||
<button className="toolbar-btn" onClick={() => {}} title="设置">
|
||
📋 设置
|
||
</button>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
{editingNode && (
|
||
<NodeEditor
|
||
node={editingNode}
|
||
onUpdate={handleUpdateNode}
|
||
onClose={() => setEditingNode(undefined)}
|
||
settings={settings}
|
||
/>
|
||
)}
|
||
|
||
{editingEdge && (
|
||
<EdgeEditor
|
||
edge={editingEdge}
|
||
onUpdate={handleUpdateEdge}
|
||
onClose={() => setEditingEdge(undefined)}
|
||
/>
|
||
)}
|
||
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function App(): ReactNode {
|
||
return (
|
||
<ReactFlowProvider>
|
||
<FlowContent />
|
||
</ReactFlowProvider>
|
||
);
|
||
}
|