diff --git a/README.md b/README.md index 18bc70e..0796ca1 100644 --- a/README.md +++ b/README.md @@ -1,16 +1 @@ -# React + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. +# Wireguard 组网可视化编辑器 diff --git a/src/TODO.md b/TODO.md similarity index 90% rename from src/TODO.md rename to TODO.md index ba3b95b..2d92769 100644 --- a/src/TODO.md +++ b/TODO.md @@ -1,7 +1,7 @@ ## 产品基本功能实现待办事项 - [x] 完成节点的编辑窗口 -- [ ] 完成边的编辑窗口 +- [x] 完成边的编辑窗口 - [ ] 完成全局设置编辑窗口以及相关联动 - [ ] 实现配置生成逻辑,并验证有效 - [ ] 实现配置保存和加载功能 diff --git a/src/App.tsx b/src/App.tsx index 66f8ab1..342f89d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,12 +14,14 @@ import { NodeMouseHandler, OnConnect, MiniMap, - IsValidConnection + IsValidConnection, + EdgeMouseHandler } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { AppNode, AppEdge, NodeData, Settings } from './types/graph'; +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'; @@ -50,7 +52,8 @@ function FlowContent(): ReactNode { const [edges, setEdges] = useState(initialEdges); const [settings, setSettings] = useState(initialSettings); - const [editingNode, setEditingNode] = useState(null); + const [editingNode, setEditingNode] = useState(undefined); + const [editingEdge, setEditingEdge] = useState(undefined); const [enableTwoWay, setEnableTwoWay] = useState(false); const onNodesChange = useCallback( @@ -63,12 +66,14 @@ function FlowContent(): ReactNode { const onConnect = useCallback( (params) => { + const id = `e-${crypto.randomUUID()}` const newEdge : AppEdge = { ...params, - id: `e-${crypto.randomUUID()}`, + id: id, animated: !enableTwoWay, markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed }, data : { + id: id, isTwoWayEdge: enableTwoWay } } @@ -80,6 +85,11 @@ function FlowContent(): ReactNode { (_event, node) => setEditingNode(node.data), []); + const onEdgeClick = useCallback>( + (_event, edge) => setEditingEdge(edge.data), + [] + ) + const validateConnection = useCallback( (connection) => { if (connection.source === connection.target) { @@ -117,7 +127,19 @@ function FlowContent(): ReactNode { return node; }) ); - setEditingNode(null); + 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 ( @@ -129,6 +151,7 @@ function FlowContent(): ReactNode { onEdgesChange={onEdgesChange} onConnect={onConnect} onNodeDoubleClick={onNodeClick} + onEdgeDoubleClick={onEdgeClick} nodeTypes={nodeTypes} deleteKeyCode={["Delete"]} fitView @@ -168,10 +191,19 @@ function FlowContent(): ReactNode { setEditingNode(null)} + onClose={() => setEditingNode(undefined)} settings={settings} /> )} + + {editingEdge && ( + setEditingEdge(undefined)} + /> + )} + ); } diff --git a/src/components/EdgeEditor.tsx b/src/components/EdgeEditor.tsx new file mode 100644 index 0000000..88861a0 --- /dev/null +++ b/src/components/EdgeEditor.tsx @@ -0,0 +1,68 @@ +import { useState, ReactNode } from 'react'; +import { AppNode, AppEdge, EdgeData } from '../types/graph'; +import { useReactFlow } from '@xyflow/react'; + +interface EdgeEditorProps { + edge: EdgeData; + onUpdate: (data: EdgeData) => void; + onClose: () => void; +} + +export default function NodeEditor({ + edge, + onUpdate, + onClose +}: EdgeEditorProps): ReactNode { + const [formData, setFormData] = useState(edge); + const { getNode, getEdge } = useReactFlow(); + + const handleInputChange = (field: keyof EdgeData, value: string): void => { + setFormData(prev => ({ + ...prev, + [field]: value + })); + }; + + const handleSave = (): void => { + onUpdate(formData); + onClose(); + }; + + const e = getEdge(edge.id) + + const source = e ? getNode(e.source) : undefined + const target = e ? getNode(e.target) : undefined + + const sourceName = source ? source.data.label : "error" + const targetName = target ? target.data.label : "error" + + const label = `[${sourceName}] ${edge.isTwoWayEdge ? "↔" : "→"} [${targetName}]` + + return ( +
+
+
+

设置边 {label} 参数

+ +
+ +
+ + handleInputChange('persistentKeepalive', e.target.value)} + placeholder={`留空或0代表不保活`} + /> +
+ +
+ + +
+
+
+ ); +} diff --git a/src/types/graph.ts b/src/types/graph.ts index b2e4230..ac5c2ab 100644 --- a/src/types/graph.ts +++ b/src/types/graph.ts @@ -21,6 +21,7 @@ export type NodeData = { } export type EdgeData = { + readonly id: string; isTwoWayEdge: boolean; persistentKeepalive?: string; }