Compare commits
2 Commits
a469133446
...
1edb3474c5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1edb3474c5 | ||
|
|
098259177b |
17
README.md
17
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 组网可视化编辑器
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
## 产品基本功能实现待办事项
|
||||
|
||||
- [x] 完成节点的编辑窗口
|
||||
- [ ] 完成边的编辑窗口
|
||||
- [x] 完成边的编辑窗口
|
||||
- [ ] 完成全局设置编辑窗口以及相关联动
|
||||
- [ ] 实现配置生成逻辑,并验证有效
|
||||
- [ ] 实现配置保存和加载功能
|
||||
44
src/App.tsx
44
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<AppEdge[]>(initialEdges);
|
||||
const [settings, setSettings] = useState<Settings>(initialSettings);
|
||||
|
||||
const [editingNode, setEditingNode] = useState<NodeData | null>(null);
|
||||
const [editingNode, setEditingNode] = useState<NodeData | undefined>(undefined);
|
||||
const [editingEdge, setEditingEdge] = useState<EdgeData | undefined>(undefined);
|
||||
const [enableTwoWay, setEnableTwoWay] = useState(false);
|
||||
|
||||
const onNodesChange = useCallback(
|
||||
@ -63,12 +66,14 @@ function FlowContent(): ReactNode {
|
||||
|
||||
const onConnect = useCallback<OnConnect>(
|
||||
(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<EdgeMouseHandler<AppEdge>>(
|
||||
(_event, edge) => setEditingEdge(edge.data),
|
||||
[]
|
||||
)
|
||||
|
||||
const validateConnection = useCallback<IsValidConnection>(
|
||||
(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 {
|
||||
<NodeEditor
|
||||
node={editingNode}
|
||||
onUpdate={handleUpdateNode}
|
||||
onClose={() => setEditingNode(null)}
|
||||
onClose={() => setEditingNode(undefined)}
|
||||
settings={settings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editingEdge && (
|
||||
<EdgeEditor
|
||||
edge={editingEdge}
|
||||
onUpdate={handleUpdateEdge}
|
||||
onClose={() => setEditingEdge(undefined)}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,9 +13,6 @@ export default function CustomNode({
|
||||
<span className="node-label">{data.label}</span>
|
||||
</div>
|
||||
|
||||
<div className="node-info">
|
||||
</div>
|
||||
|
||||
{[Position.Top, Position.Bottom, Position.Right, Position.Left].map((position) => (
|
||||
(["target", "source"] as const).map((type) => (
|
||||
<Handle type={type} position={position} id={position} key={`${type}-${position}`} className="node-handle"/>
|
||||
|
||||
68
src/components/EdgeEditor.tsx
Normal file
68
src/components/EdgeEditor.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { AppNode, AppEdge, EdgeData, EdgeDataUpdate } from '../types/graph';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
import './FormEditor.css';
|
||||
|
||||
interface EdgeEditorProps {
|
||||
edge: EdgeData;
|
||||
onUpdate: (data: EdgeDataUpdate) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function NodeEditor({
|
||||
edge,
|
||||
onUpdate,
|
||||
onClose
|
||||
}: EdgeEditorProps): ReactNode {
|
||||
|
||||
const [keepalive, setKeepalive] = useState(edge.persistentKeepalive);
|
||||
const { getNode, getEdge } = useReactFlow<AppNode, AppEdge>();
|
||||
|
||||
const handleSave = (): void => {
|
||||
onUpdate({persistentKeepalive : keepalive});
|
||||
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 (
|
||||
<div className="node-editor-overlay">
|
||||
<div className="node-editor">
|
||||
<div className="editor-header">
|
||||
<h2>设置边 {label} 参数</h2>
|
||||
<button className="close-btn" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>保活频率(单位:秒)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={keepalive || ''}
|
||||
onChange={
|
||||
(e) => {
|
||||
const value = e.target.valueAsNumber;
|
||||
setKeepalive(isNaN(value) ? undefined : value);
|
||||
}
|
||||
}
|
||||
placeholder={`留空或0代表不保活`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="editor-actions">
|
||||
<button className="btn-save" onClick={handleSave}>保存</button>
|
||||
<button className="btn-cancel" onClick={onClose}>取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { NodeData, Settings } from '../types/graph';
|
||||
import { NodeData, Settings, NodeDataUpdate } from '../types/graph';
|
||||
import { generateWireGuardPrivateKey } from '../utils/wireguardConfig'
|
||||
import './NodeEditor.css';
|
||||
import './FormEditor.css';
|
||||
import Folder from './Folder'
|
||||
|
||||
|
||||
@ -10,15 +10,10 @@ interface Validation {
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
function validateNodeConfig(formData : NodeData) : Validation {
|
||||
// todo
|
||||
return {isValid : true, errors: []}
|
||||
}
|
||||
|
||||
interface NodeEditorProps {
|
||||
node: NodeData;
|
||||
settings: Settings;
|
||||
onUpdate: (data: NodeData) => void;
|
||||
onUpdate: (data: NodeDataUpdate) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@ -29,36 +24,48 @@ export default function NodeEditor({
|
||||
onClose
|
||||
}: NodeEditorProps): ReactNode {
|
||||
|
||||
const [formData, setFormData] = useState<NodeData>(node);
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
|
||||
const handleInputChange = (field: keyof NodeData, value: string): void => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
const [label, setLabel] = useState<string>(node.label);
|
||||
const [privateKey, setPrivateKey] = useState<string>(node.privateKey);
|
||||
const [ipv4Address, setIpv4Address] = useState(node.ipv4Address);
|
||||
const [ipv6Address, setIpv6Address] = useState(node.ipv6Address);
|
||||
const [disallowIPs, setDisallowIPs] = useState(node.disallowIPs);
|
||||
const [listenPort, setListenPort] = useState(node.listenPort);
|
||||
const [mtu, setmtu] = useState(node.mtu);
|
||||
const [dnsServers, setdnsServers] = useState(node.dnsServers)
|
||||
const [postUp, setPostUp] = useState(node.postUp)
|
||||
const [postDown, setPostDown] = useState(node.postDown)
|
||||
const [notes, setNotes] = useState(node.notes)
|
||||
|
||||
const handleSave = (): void => {
|
||||
const validation = validateNodeConfig(formData);
|
||||
if (!validation.isValid) {
|
||||
setErrors(validation.errors);
|
||||
return;
|
||||
}
|
||||
// const validation = validateNodeConfig(formData);
|
||||
// if (!validation.isValid) {
|
||||
// setErrors(validation.errors);
|
||||
// return;
|
||||
// }
|
||||
setErrors([]);
|
||||
onUpdate(formData);
|
||||
onUpdate({
|
||||
label: label,
|
||||
privateKey: privateKey,
|
||||
ipv4Address: ipv4Address,
|
||||
ipv6Address: ipv6Address,
|
||||
disallowIPs: disallowIPs,
|
||||
postUp: postUp,
|
||||
postDown: postDown,
|
||||
mtu: mtu,
|
||||
listenPort: listenPort,
|
||||
dnsServers: dnsServers,
|
||||
notes: notes
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleGenerateKey = (): void => {
|
||||
handleInputChange('privateKey', generateWireGuardPrivateKey())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="node-editor-overlay">
|
||||
<div className="node-editor">
|
||||
<div className="editor-header">
|
||||
<h2>编辑节点: {formData.label || '新节点'}</h2>
|
||||
<h2>编辑节点: {label}</h2>
|
||||
<button className="close-btn" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
@ -80,8 +87,8 @@ export default function NodeEditor({
|
||||
<label>节点名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.label || ''}
|
||||
onChange={(e) => handleInputChange('label', e.target.value)}
|
||||
value={label}
|
||||
onChange={e => setLabel(e.target.value)}
|
||||
placeholder="例如: Node-A"
|
||||
/>
|
||||
</div>
|
||||
@ -90,10 +97,11 @@ export default function NodeEditor({
|
||||
<label>私钥</label>
|
||||
<div className="item-group">
|
||||
<input
|
||||
value={formData.privateKey || ''}
|
||||
value={privateKey}
|
||||
readOnly
|
||||
/>
|
||||
<button className="btn-interect" onClick={handleGenerateKey}>重新生成</button>
|
||||
<button className="btn-interect"
|
||||
onClick={_ => setPrivateKey(generateWireGuardPrivateKey())}>重新生成</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -102,8 +110,8 @@ export default function NodeEditor({
|
||||
<label>IPv4地址</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.ipv4Address || ''}
|
||||
onChange={(e) => handleInputChange('ipv4Address', e.target.value)}
|
||||
value={ipv4Address || ''}
|
||||
onChange={e => setIpv4Address(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -113,8 +121,8 @@ export default function NodeEditor({
|
||||
<label>IPv6地址</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.ipv6Address || ''}
|
||||
onChange={(e) => handleInputChange('ipv6Address', e.target.value)}
|
||||
value={ipv6Address || ''}
|
||||
onChange={e => setIpv6Address(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -124,8 +132,8 @@ export default function NodeEditor({
|
||||
<label>子网黑名单</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.disallowIPs || ''}
|
||||
onChange={(e) => handleInputChange('disallowIPs', e.target.value)}
|
||||
value={disallowIPs || ''}
|
||||
onChange={e => setDisallowIPs(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -136,8 +144,11 @@ export default function NodeEditor({
|
||||
min="1024"
|
||||
max="49151"
|
||||
step="1"
|
||||
value={formData.listenPort || ''}
|
||||
onChange={(e) => handleInputChange('listenPort', e.target.value)}
|
||||
value={listenPort || ''}
|
||||
onChange={e => {
|
||||
const value = e.target.valueAsNumber;
|
||||
setListenPort(isNaN(value) ? undefined : value);
|
||||
}}
|
||||
placeholder={`默认值:${settings.listenPort}`}
|
||||
/>
|
||||
</div>
|
||||
@ -148,8 +159,11 @@ export default function NodeEditor({
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={formData.mtu || ''}
|
||||
onChange={(e) => handleInputChange('mtu', e.target.value)}
|
||||
value={mtu || ''}
|
||||
onChange={e => {
|
||||
const value = e.target.valueAsNumber;
|
||||
setmtu(isNaN(value) ? undefined : value);
|
||||
}}
|
||||
placeholder={settings.mtu ? `默认值:${settings.mtu}` : ''}
|
||||
/>
|
||||
</div>
|
||||
@ -158,8 +172,8 @@ export default function NodeEditor({
|
||||
<label>DNS服务器</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.dnsServers || ''}
|
||||
onChange={(e) => handleInputChange('dnsServers', e.target.value)}
|
||||
value={dnsServers || ''}
|
||||
onChange={(e) => setdnsServers(e.target.value)}
|
||||
placeholder="例如: 8.8.8.8,1.1.1.1"
|
||||
/>
|
||||
</div>
|
||||
@ -168,8 +182,8 @@ export default function NodeEditor({
|
||||
<label>PostUp</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={formData.postUp || ''}
|
||||
onChange={(e) => handleInputChange('postUp', e.target.value)}
|
||||
value={postUp || ''}
|
||||
onChange={(e) => setPostUp(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -177,8 +191,8 @@ export default function NodeEditor({
|
||||
<label>PostDown</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={formData.postDown || ''}
|
||||
onChange={(e) => handleInputChange('postDown', e.target.value)}
|
||||
value={postDown || ''}
|
||||
onChange={(e) => setPostDown(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -186,8 +200,8 @@ export default function NodeEditor({
|
||||
<label>备注</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
value={formData.notes || ''}
|
||||
onChange={(e) => handleInputChange('notes', e.target.value)}
|
||||
value={notes || ''}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Folder>
|
||||
|
||||
@ -11,7 +11,20 @@ export type NodeData = {
|
||||
ipv4Address?: string;
|
||||
ipv6Address?: string;
|
||||
disallowIPs?: string;
|
||||
postUp?: string;
|
||||
postDown?: string;
|
||||
mtu?: number;
|
||||
listenPort?: number;
|
||||
dnsServers?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface NodeDataUpdate {
|
||||
label: string;
|
||||
privateKey: string;
|
||||
ipv4Address?: string;
|
||||
ipv6Address?: string;
|
||||
disallowIPs?: string;
|
||||
postUp?: string;
|
||||
postDown?: string;
|
||||
mtu?: number;
|
||||
@ -21,8 +34,13 @@ export type NodeData = {
|
||||
}
|
||||
|
||||
export type EdgeData = {
|
||||
readonly id: string;
|
||||
isTwoWayEdge: boolean;
|
||||
persistentKeepalive?: string;
|
||||
persistentKeepalive?: number;
|
||||
}
|
||||
|
||||
export interface EdgeDataUpdate {
|
||||
persistentKeepalive?: number;
|
||||
}
|
||||
|
||||
export class SubNetRouter {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user