敲定结构
This commit is contained in:
parent
b18e21b620
commit
155fd2724a
@ -98,4 +98,12 @@
|
|||||||
transform: translateY(1px) scale(0.995);
|
transform: translateY(1px) scale(0.995);
|
||||||
background-color: #0f62d6; /* slightly darker on press */
|
background-color: #0f62d6; /* slightly darker on press */
|
||||||
box-shadow: inset 0 1px 2px rgba(0,0,0,0.12);
|
box-shadow: inset 0 1px 2px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
font-size: 8px;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
@ -216,6 +216,7 @@ export default function CustomNode({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const empty = !(data.ipv4Address || data.ipv6Address);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`custom-node ${selected ? 'selected' : ''}`}>
|
<div className={`custom-node ${selected ? 'selected' : ''}`}>
|
||||||
@ -224,20 +225,22 @@ export default function CustomNode({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='node-info'>
|
<div className='node-info'>
|
||||||
<div className="info-item">
|
{empty && <div className="empty-tip">未配置地址信息</div>}
|
||||||
|
|
||||||
|
{data.ipv4Address && <div className="info-item">
|
||||||
<span className="label">IPv4地址:</span>
|
<span className="label">IPv4地址:</span>
|
||||||
<span className="value">{data.ipv4Address || "未设置"}</span>
|
<span className="value">{data.ipv4Address || "未设置"}</span>
|
||||||
</div>
|
</div>}
|
||||||
|
|
||||||
<div className="info-item">
|
{data.ipv6Address && <div className="info-item">
|
||||||
<span className="label">IPv6地址:</span>
|
<span className="label">IPv6地址:</span>
|
||||||
<span className="value">{data.ipv6Address || "未设置"}</span>
|
<span className="value">{data.ipv6Address || "未设置"}</span>
|
||||||
</div>
|
</div>}
|
||||||
|
|
||||||
<div className="info-item">
|
{data.listenAddress && <div className="info-item">
|
||||||
<span className="label">监听地址:</span>
|
<span className="label">监听地址:</span>
|
||||||
<span className="value">{data.listenAddress || "未设置"}</span>
|
<span className="value">{data.listenAddress || "未设置"}</span>
|
||||||
</div>
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="node-actions">
|
<div className="node-actions">
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { useState, ReactNode } from 'react';
|
import { useState, ReactNode } from 'react';
|
||||||
import { Settings } from '../types/graph';
|
import { Settings, SubnetInfo } from '../types/graph';
|
||||||
import './FormEditor.css';
|
import './FormEditor.css';
|
||||||
import {IPUtils} from '../utils/iputils'
|
import Folder from './Folder';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { IPUtils } from '../utils/iputils';
|
||||||
|
|
||||||
interface SettingEditorProps {
|
interface SettingEditorProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
@ -38,11 +40,28 @@ export default function SettingsEditor({
|
|||||||
|
|
||||||
const [listenPort, setListenPort] = useState<number>(settings.listenPort);
|
const [listenPort, setListenPort] = useState<number>(settings.listenPort);
|
||||||
const [mtu, setmtu] = useState<number>(settings.mtu);
|
const [mtu, setmtu] = useState<number>(settings.mtu);
|
||||||
|
const [subnets, setSubnets] = useState<SubnetInfo[]>(settings.subnets);
|
||||||
|
const [subnetInput, setSubnetInput] = useState<string>("");
|
||||||
|
|
||||||
|
const handleAddSubnet = (cidrStr: string) => {
|
||||||
|
const result = IPUtils.parse(cidrStr);
|
||||||
|
const cidr = result.cidr;
|
||||||
|
if(!cidr) {
|
||||||
|
toast.error(`无效的CIDR格式: ${result.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(subnets.some(s => IPUtils.equal(s.subnet, cidr))) {
|
||||||
|
toast.error("该CIDR已存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubnets([...subnets, {subnet: cidr, nodes: {}}]);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = (): void => {
|
const handleSave = (): void => {
|
||||||
const updateData = {
|
const updateData = {
|
||||||
listenPort: listenPort,
|
listenPort: listenPort,
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
|
subnets: subnets
|
||||||
};
|
};
|
||||||
|
|
||||||
const validation = Validate(updateData);
|
const validation = Validate(updateData);
|
||||||
@ -100,6 +119,49 @@ export default function SettingsEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>子网配置</label>
|
||||||
|
<div className="add-subnet">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="subnet-input"
|
||||||
|
placeholder="CIDR格式的子网,例如:192.168.1.0/24"
|
||||||
|
value={subnetInput}
|
||||||
|
onChange={e => setSubnetInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn-add" onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleAddSubnet(subnetInput);
|
||||||
|
}}>+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="subnet-list">
|
||||||
|
<label>已添加的子网:</label>
|
||||||
|
{subnets.map((subnetInfo, idx) => (
|
||||||
|
<Folder key={idx} title={subnetInfo.subnet.toString()}>
|
||||||
|
<div className="add-node-to-subnet">
|
||||||
|
<select>
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option value="apple">苹果</option>
|
||||||
|
<option value="banana">香蕉</option>
|
||||||
|
<option value="orange">橘子</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" placeholder="输入节点ID" />
|
||||||
|
<button>+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="subnet-nodes-list">
|
||||||
|
<li>
|
||||||
|
<span>节点ID: example-node-1, IP:</span>
|
||||||
|
<button className="remove-node-btn">×</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Folder>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="editor-actions">
|
<div className="editor-actions">
|
||||||
<button className="btn-save" onClick={handleSave}>保存</button>
|
<button className="btn-save" onClick={handleSave}>保存</button>
|
||||||
<button className="btn-cancel" onClick={onClose}>取消</button>
|
<button className="btn-cancel" onClick={onClose}>取消</button>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Node, Edge } from '@xyflow/react';
|
import { Node, Edge } from '@xyflow/react';
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
import { CIDR } from '../utils/iputils';
|
||||||
|
|
||||||
export type AppNode = Node<NodeData>;
|
export type AppNode = Node<NodeData>;
|
||||||
|
|
||||||
@ -33,14 +34,22 @@ export type EdgeDataUpdate = {
|
|||||||
persistentKeepalive?: number;
|
persistentKeepalive?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SubnetInfo {
|
||||||
|
subnet: CIDR;
|
||||||
|
nodes: Record<string, CIDR | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
listenPort: number;
|
listenPort: number;
|
||||||
mtu: number;
|
mtu: number;
|
||||||
|
|
||||||
|
subnets: SubnetInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialSettings : Settings = {
|
export const initialSettings : Settings = {
|
||||||
listenPort: 38894,
|
listenPort: 38894,
|
||||||
mtu: 1420,
|
mtu: 1420,
|
||||||
|
subnets: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsContext = createContext<Settings>(initialSettings);
|
export const SettingsContext = createContext<Settings>(initialSettings);
|
||||||
@ -125,6 +125,10 @@ export class IPUtils {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static equal(c1: CIDR, c2: CIDR): boolean {
|
||||||
|
return c1.version === c2.version && c1.binary === c2.binary && c1.mask === c2.mask;
|
||||||
|
}
|
||||||
|
|
||||||
static parse(cidrStr: string): CIDRParseResult {
|
static parse(cidrStr: string): CIDRParseResult {
|
||||||
const parts = cidrStr.split('/');
|
const parts = cidrStr.split('/');
|
||||||
if (parts.length !== 2) return { error: 'Invalid CIDR format' };
|
if (parts.length !== 2) return { error: 'Invalid CIDR format' };
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user