266 lines
7.2 KiB
TypeScript
266 lines
7.2 KiB
TypeScript
import { useState, ReactNode } from 'react';
|
||
import { Settings, SubnetInfo, AppEdge, AppNode } from '../types/graph';
|
||
import { useReactFlow} from '@xyflow/react';
|
||
import './FormEditor.css';
|
||
import Folder from './Folder';
|
||
import toast from 'react-hot-toast';
|
||
import { IPUtils } from '../utils/iputils';
|
||
import Select from 'react-select';
|
||
|
||
interface SettingEditorProps {
|
||
settings: Settings;
|
||
onUpdate: (data: Settings) => void;
|
||
onClose: () => void;
|
||
}
|
||
|
||
interface SelectionOption {
|
||
value: string;
|
||
label: string;
|
||
}
|
||
|
||
interface SubnetProps {
|
||
subnets: SubnetInfo[],
|
||
setSubnets: (subnets: SubnetInfo[]) => void,
|
||
index: number,
|
||
}
|
||
|
||
function Validate(updateData: Settings) : string[] {
|
||
const errors: string[] = [];
|
||
|
||
const {mtu, listenPort} = updateData;
|
||
|
||
if(isNaN(listenPort)) {
|
||
errors.push("监听端口不是数字");
|
||
} else if(listenPort < 30000 || listenPort > 49151) {
|
||
errors.push("监听端口不在范围内:[30000, 49151]");
|
||
}
|
||
|
||
if(isNaN(mtu)) {
|
||
errors.push("mtu不是数字");
|
||
} else if(mtu < 1200) {
|
||
errors.push("mtu过小(小于1200)");
|
||
}
|
||
|
||
return errors;
|
||
}
|
||
|
||
function SubnetItem({index, subnets, setSubnets} : SubnetProps) : ReactNode {
|
||
const subnetInfo = subnets[index];
|
||
const [selectedOption, setSelectedOption] = useState<SelectionOption | null>(null);
|
||
const [nodeSubnet, setNodeSubnet] = useState<string>("");
|
||
const { getNodes, getNode } = useReactFlow<AppNode, AppEdge>();
|
||
|
||
const options : SelectionOption[] = getNodes().map(node => ({
|
||
value: node.id,
|
||
label: node.data.label
|
||
}));
|
||
|
||
const handleAddNodeSubnet = () => {
|
||
if(!selectedOption) {
|
||
toast.error("没有选择有效的节点");
|
||
return ;
|
||
}
|
||
const nodeId = selectedOption.value;
|
||
|
||
const nodecidr = (() => {
|
||
if(nodeSubnet.includes("/")) return nodeSubnet;
|
||
return `${nodeSubnet}/${nodeSubnet.includes(".") ? "32" : "128"}`;
|
||
})();
|
||
|
||
const result = IPUtils.parse(nodecidr);
|
||
const cidr = result.cidr;
|
||
if(!cidr) {
|
||
toast.error(`无法解析子网:${result.error}`);
|
||
return ;
|
||
}
|
||
|
||
if(!subnetInfo.subnet.contains(cidr)) {
|
||
toast.error("不在子网范围内");
|
||
return ;
|
||
}
|
||
|
||
if(subnetInfo.nodes.some(node => node.nodeId === nodeId)) {
|
||
toast.error(`节点已添加`);
|
||
return;
|
||
}
|
||
|
||
setSubnets(subnets.map((info, idx) => {
|
||
if(idx === index) {
|
||
return {...info, nodes: [...info.nodes, {nodeId: nodeId, cidr: cidr}]}
|
||
}
|
||
return info;
|
||
}));
|
||
}
|
||
|
||
const handleRemoveNodeSubnet = (nodeId: string) => {
|
||
setSubnets(subnets.map((info, idx) => {
|
||
if(idx === index) {
|
||
return {...info, nodes: info.nodes.filter(node => node.nodeId != nodeId)};
|
||
}
|
||
return info;
|
||
}));
|
||
}
|
||
|
||
const handleRemoveSubnet = () => {
|
||
setSubnets(subnets.filter((_, idx) => idx != index));
|
||
}
|
||
|
||
return (<Folder title={subnetInfo.subnet.toString()}>
|
||
<div>
|
||
<Select
|
||
onChange={setSelectedOption}
|
||
options={options}
|
||
placeholder="请选择节点..."
|
||
/>
|
||
<input
|
||
type="text"
|
||
placeholder="输入子网"
|
||
value={nodeSubnet}
|
||
onChange={e => {setNodeSubnet(e.target.value)}}/>
|
||
<button onClick={_ => handleAddNodeSubnet()}>+</button>
|
||
</div>
|
||
|
||
{subnetInfo.nodes.map((nodeInfo, _) => {
|
||
const node = getNode(nodeInfo.nodeId);
|
||
if(!node) return <></>
|
||
return (
|
||
<div>
|
||
<label>- {`${node.data.label}: ${nodeInfo.cidr?.toString()}`}</label>
|
||
<button onClick={_ => handleRemoveNodeSubnet(nodeInfo.nodeId)}>删除</button>
|
||
</div>
|
||
)
|
||
})}
|
||
|
||
<div>
|
||
<button onClick={_ => handleRemoveSubnet()}>删除子网</button>
|
||
</div>
|
||
|
||
</Folder>
|
||
);
|
||
}
|
||
|
||
export default function SettingsEditor({
|
||
settings,
|
||
onUpdate,
|
||
onClose
|
||
}: SettingEditorProps): ReactNode {
|
||
const [errors, setErrors] = useState<string[]>([]);
|
||
|
||
const [listenPort, setListenPort] = useState<number>(settings.listenPort);
|
||
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 updateData = {
|
||
listenPort: listenPort,
|
||
mtu: mtu,
|
||
subnets: subnets
|
||
};
|
||
|
||
const validation = Validate(updateData);
|
||
setErrors(validation);
|
||
if(validation.length > 0) {
|
||
return ;
|
||
}
|
||
|
||
onUpdate(updateData);
|
||
onClose();
|
||
};
|
||
|
||
return (
|
||
<div className="node-editor-overlay">
|
||
<div className="node-editor">
|
||
<div className="editor-header">
|
||
<h2>全局设置</h2>
|
||
<button className="close-btn" onClick={onClose}>×</button>
|
||
</div>
|
||
|
||
{errors.length > 0 && (
|
||
<div className="error-box">
|
||
<div
|
||
className="error-close-btn"
|
||
onClick={() => setErrors([])} // 点击清空错误数组
|
||
title="关闭提示"
|
||
>×</div>
|
||
|
||
{errors.map((error, idx) => (
|
||
<p key={idx} className="error-message">• {error}</p>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
<div className="form-group">
|
||
<label>监听端口</label>
|
||
<input
|
||
type="number"
|
||
min="30000"
|
||
max="49151"
|
||
step="1"
|
||
value={listenPort || ""}
|
||
onChange={e => setListenPort(e.target.valueAsNumber)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>mtu</label>
|
||
<input
|
||
type="number"
|
||
min="1200"
|
||
step="1"
|
||
value={mtu || ''}
|
||
onChange={e => setmtu(e.target.valueAsNumber)}
|
||
/>
|
||
</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((_, idx) =>
|
||
<SubnetItem
|
||
key={idx}
|
||
index={idx}
|
||
subnets={subnets}
|
||
setSubnets={setSubnets}/>)}
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div className="editor-actions">
|
||
<button className="btn-save" onClick={handleSave}>保存</button>
|
||
<button className="btn-cancel" onClick={onClose}>取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|