Compare commits

..

No commits in common. "1edb3474c556c82be5c83084f7b38ba5cc7801de" and "a469133446ee93053ba0f312c16811c09f529507" have entirely different histories.

8 changed files with 314 additions and 428 deletions

View File

@ -1 +1,16 @@
# Wireguard 组网可视化编辑器
# 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.

View File

@ -14,14 +14,12 @@ import {
NodeMouseHandler,
OnConnect,
MiniMap,
IsValidConnection,
EdgeMouseHandler
IsValidConnection
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { AppNode, AppEdge, NodeData, EdgeData, Settings } from './types/graph';
import { AppNode, AppEdge, NodeData, 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';
@ -52,8 +50,7 @@ function FlowContent(): ReactNode {
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 [editingNode, setEditingNode] = useState<NodeData | null>(null);
const [enableTwoWay, setEnableTwoWay] = useState(false);
const onNodesChange = useCallback(
@ -66,14 +63,12 @@ function FlowContent(): ReactNode {
const onConnect = useCallback<OnConnect>(
(params) => {
const id = `e-${crypto.randomUUID()}`
const newEdge : AppEdge = {
...params,
id: id,
id: `e-${crypto.randomUUID()}`,
animated: !enableTwoWay,
markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed },
data : {
id: id,
isTwoWayEdge: enableTwoWay
}
}
@ -85,11 +80,6 @@ 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) {
@ -127,19 +117,7 @@ function FlowContent(): ReactNode {
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);
setEditingNode(null);
};
return (
@ -151,7 +129,6 @@ function FlowContent(): ReactNode {
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeDoubleClick={onNodeClick}
onEdgeDoubleClick={onEdgeClick}
nodeTypes={nodeTypes}
deleteKeyCode={["Delete"]}
fitView
@ -191,19 +168,10 @@ function FlowContent(): ReactNode {
<NodeEditor
node={editingNode}
onUpdate={handleUpdateNode}
onClose={() => setEditingNode(undefined)}
onClose={() => setEditingNode(null)}
settings={settings}
/>
)}
{editingEdge && (
<EdgeEditor
edge={editingEdge}
onUpdate={handleUpdateEdge}
onClose={() => setEditingEdge(undefined)}
/>
)}
</div>
);
}

View File

@ -1,7 +1,7 @@
## 产品基本功能实现待办事项
- [x] 完成节点的编辑窗口
- [x] 完成边的编辑窗口
- [ ] 完成边的编辑窗口
- [ ] 完成全局设置编辑窗口以及相关联动
- [ ] 实现配置生成逻辑,并验证有效
- [ ] 实现配置保存和加载功能

View File

@ -13,6 +13,9 @@ 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"/>

View File

@ -1,68 +0,0 @@
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>
);
}

View File

@ -1,7 +1,7 @@
import { useState, ReactNode } from 'react';
import { NodeData, Settings, NodeDataUpdate } from '../types/graph';
import { NodeData, Settings } from '../types/graph';
import { generateWireGuardPrivateKey } from '../utils/wireguardConfig'
import './FormEditor.css';
import './NodeEditor.css';
import Folder from './Folder'
@ -10,10 +10,15 @@ interface Validation {
errors: string[]
}
function validateNodeConfig(formData : NodeData) : Validation {
// todo
return {isValid : true, errors: []}
}
interface NodeEditorProps {
node: NodeData;
settings: Settings;
onUpdate: (data: NodeDataUpdate) => void;
onUpdate: (data: NodeData) => void;
onClose: () => void;
}
@ -24,48 +29,36 @@ export default function NodeEditor({
onClose
}: NodeEditorProps): ReactNode {
const [formData, setFormData] = useState<NodeData>(node);
const [errors, setErrors] = useState<string[]>([]);
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 handleInputChange = (field: keyof NodeData, value: string): void => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
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({
label: label,
privateKey: privateKey,
ipv4Address: ipv4Address,
ipv6Address: ipv6Address,
disallowIPs: disallowIPs,
postUp: postUp,
postDown: postDown,
mtu: mtu,
listenPort: listenPort,
dnsServers: dnsServers,
notes: notes
});
onUpdate(formData);
onClose();
};
const handleGenerateKey = (): void => {
handleInputChange('privateKey', generateWireGuardPrivateKey())
}
return (
<div className="node-editor-overlay">
<div className="node-editor">
<div className="editor-header">
<h2>: {label}</h2>
<h2>: {formData.label || '新节点'}</h2>
<button className="close-btn" onClick={onClose}>×</button>
</div>
@ -87,8 +80,8 @@ export default function NodeEditor({
<label></label>
<input
type="text"
value={label}
onChange={e => setLabel(e.target.value)}
value={formData.label || ''}
onChange={(e) => handleInputChange('label', e.target.value)}
placeholder="例如: Node-A"
/>
</div>
@ -97,11 +90,10 @@ export default function NodeEditor({
<label></label>
<div className="item-group">
<input
value={privateKey}
value={formData.privateKey || ''}
readOnly
/>
<button className="btn-interect"
onClick={_ => setPrivateKey(generateWireGuardPrivateKey())}></button>
<button className="btn-interect" onClick={handleGenerateKey}></button>
</div>
</div>
@ -110,8 +102,8 @@ export default function NodeEditor({
<label>IPv4地址</label>
<input
type="text"
value={ipv4Address || ''}
onChange={e => setIpv4Address(e.target.value)}
value={formData.ipv4Address || ''}
onChange={(e) => handleInputChange('ipv4Address', e.target.value)}
/>
</div>
)}
@ -121,8 +113,8 @@ export default function NodeEditor({
<label>IPv6地址</label>
<input
type="text"
value={ipv6Address || ''}
onChange={e => setIpv6Address(e.target.value)}
value={formData.ipv6Address || ''}
onChange={(e) => handleInputChange('ipv6Address', e.target.value)}
/>
</div>
)}
@ -132,8 +124,8 @@ export default function NodeEditor({
<label></label>
<input
type="text"
value={disallowIPs || ''}
onChange={e => setDisallowIPs(e.target.value)}
value={formData.disallowIPs || ''}
onChange={(e) => handleInputChange('disallowIPs', e.target.value)}
/>
</div>
@ -144,11 +136,8 @@ export default function NodeEditor({
min="1024"
max="49151"
step="1"
value={listenPort || ''}
onChange={e => {
const value = e.target.valueAsNumber;
setListenPort(isNaN(value) ? undefined : value);
}}
value={formData.listenPort || ''}
onChange={(e) => handleInputChange('listenPort', e.target.value)}
placeholder={`默认值:${settings.listenPort}`}
/>
</div>
@ -159,11 +148,8 @@ export default function NodeEditor({
type="number"
min="1"
step="1"
value={mtu || ''}
onChange={e => {
const value = e.target.valueAsNumber;
setmtu(isNaN(value) ? undefined : value);
}}
value={formData.mtu || ''}
onChange={(e) => handleInputChange('mtu', e.target.value)}
placeholder={settings.mtu ? `默认值:${settings.mtu}` : ''}
/>
</div>
@ -172,8 +158,8 @@ export default function NodeEditor({
<label>DNS服务器</label>
<input
type="text"
value={dnsServers || ''}
onChange={(e) => setdnsServers(e.target.value)}
value={formData.dnsServers || ''}
onChange={(e) => handleInputChange('dnsServers', e.target.value)}
placeholder="例如: 8.8.8.8,1.1.1.1"
/>
</div>
@ -182,8 +168,8 @@ export default function NodeEditor({
<label>PostUp</label>
<textarea
rows={2}
value={postUp || ''}
onChange={(e) => setPostUp(e.target.value)}
value={formData.postUp || ''}
onChange={(e) => handleInputChange('postUp', e.target.value)}
/>
</div>
@ -191,8 +177,8 @@ export default function NodeEditor({
<label>PostDown</label>
<textarea
rows={2}
value={postDown || ''}
onChange={(e) => setPostDown(e.target.value)}
value={formData.postDown || ''}
onChange={(e) => handleInputChange('postDown', e.target.value)}
/>
</div>
@ -200,8 +186,8 @@ export default function NodeEditor({
<label></label>
<textarea
rows={4}
value={notes || ''}
onChange={(e) => setNotes(e.target.value)}
value={formData.notes || ''}
onChange={(e) => handleInputChange('notes', e.target.value)}
/>
</div>
</Folder>

View File

@ -11,20 +11,7 @@ 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;
@ -34,13 +21,8 @@ export interface NodeDataUpdate {
}
export type EdgeData = {
readonly id: string;
isTwoWayEdge: boolean;
persistentKeepalive?: number;
}
export interface EdgeDataUpdate {
persistentKeepalive?: number;
persistentKeepalive?: string;
}
export class SubNetRouter {