mirror of
https://github.com/Jerryplusy/AI-powered-switches.git
synced 2025-12-05 18:21:57 +00:00
Compare commits
No commits in common. "2980489b5bc2bc2c7d3840693f5ca78f5b052fe6" and "c0bb0e6a293304604fa1631a821d9e047f29445a" have entirely different histories.
2980489b5b
...
c0bb0e6a29
@ -50,19 +50,13 @@ async def favicon():
|
|||||||
class BatchConfigRequest(BaseModel):
|
class BatchConfigRequest(BaseModel):
|
||||||
config: dict
|
config: dict
|
||||||
switch_ips: List[str] # 支持多个IP
|
switch_ips: List[str] # 支持多个IP
|
||||||
username: str = None # 添加用户名参数
|
|
||||||
password: str = None # 添加密码参数
|
|
||||||
timeout: int = None
|
|
||||||
|
|
||||||
@router.post("/batch_apply_config")
|
@router.post("/batch_apply_config")
|
||||||
async def batch_apply_config(request: BatchConfigRequest):
|
async def batch_apply_config(request: BatchConfigRequest):
|
||||||
results = {}
|
results = {}
|
||||||
for ip in request.switch_ips:
|
for ip in request.switch_ips:
|
||||||
try:
|
try:
|
||||||
configurator = SwitchConfigurator(
|
configurator = SwitchConfigurator()
|
||||||
username=request.username,
|
|
||||||
password=request.password,
|
|
||||||
timeout=request.timeout )
|
|
||||||
results[ip] = await configurator.apply_config(ip, request.config)
|
results[ip] = await configurator.apply_config(ip, request.config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results[ip] = str(e)
|
results[ip] = str(e)
|
||||||
@ -98,9 +92,6 @@ class CommandRequest(BaseModel):
|
|||||||
class ConfigRequest(BaseModel):
|
class ConfigRequest(BaseModel):
|
||||||
config: dict
|
config: dict
|
||||||
switch_ip: str
|
switch_ip: str
|
||||||
username: str = None
|
|
||||||
password: str = None
|
|
||||||
timeout: int = None
|
|
||||||
|
|
||||||
@router.post("/parse_command", response_model=dict)
|
@router.post("/parse_command", response_model=dict)
|
||||||
async def parse_command(request: CommandRequest):
|
async def parse_command(request: CommandRequest):
|
||||||
@ -124,9 +115,9 @@ async def apply_config(request: ConfigRequest):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
configurator = SwitchConfigurator(
|
configurator = SwitchConfigurator(
|
||||||
username=request.username,
|
username=settings.SWITCH_USERNAME,
|
||||||
password=request.password,
|
password=settings.SWITCH_PASSWORD,
|
||||||
timeout=request.timeout
|
timeout=settings.SWITCH_TIMEOUT
|
||||||
)
|
)
|
||||||
result = await configurator.safe_apply(request.switch_ip, request.config)
|
result = await configurator.safe_apply(request.switch_ip, request.config)
|
||||||
return {"success": True, "result": result}
|
return {"success": True, "result": result}
|
||||||
@ -142,40 +133,16 @@ class CLICommandRequest(BaseModel):
|
|||||||
commands: List[str] # 前端生成的CLI命令列表
|
commands: List[str] # 前端生成的CLI命令列表
|
||||||
is_ensp: bool = False # 是否为eNSP模拟器模式
|
is_ensp: bool = False # 是否为eNSP模拟器模式
|
||||||
|
|
||||||
# 添加方法从commands中提取凭据
|
|
||||||
def extract_credentials(self) -> tuple:
|
|
||||||
"""从commands中提取用户名和密码"""
|
|
||||||
username = None
|
|
||||||
password = None
|
|
||||||
|
|
||||||
for cmd in self.commands:
|
|
||||||
if cmd.startswith("!username="):
|
|
||||||
username = cmd.split("=")[1]
|
|
||||||
elif cmd.startswith("!password="):
|
|
||||||
password = cmd.split("=")[1]
|
|
||||||
|
|
||||||
return username, password
|
|
||||||
|
|
||||||
def get_clean_commands(self) -> List[str]:
|
|
||||||
"""获取去除凭据后的实际命令"""
|
|
||||||
return [cmd for cmd in self.commands
|
|
||||||
if not (cmd.startswith("!username=") or cmd.startswith("!password="))]
|
|
||||||
|
|
||||||
@router.post("/execute_cli_commands", response_model=dict)
|
@router.post("/execute_cli_commands", response_model=dict)
|
||||||
async def execute_cli_commands(request: CLICommandRequest):
|
async def execute_cli_commands(request: CLICommandRequest):
|
||||||
"""
|
"""
|
||||||
执行前端生成的CLI命令
|
执行前端生成的CLI命令
|
||||||
支持在commands中嵌入凭据:
|
|
||||||
!username=admin
|
|
||||||
!password=cisco123
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
username, password = request.extract_credentials()
|
|
||||||
clean_commands = request.get_clean_commands()
|
|
||||||
|
|
||||||
configurator = SwitchConfigurator(
|
configurator = SwitchConfigurator(
|
||||||
username=username,
|
username=settings.SWITCH_USERNAME,
|
||||||
password=password,
|
password=settings.SWITCH_PASSWORD,
|
||||||
timeout=settings.SWITCH_TIMEOUT,
|
timeout=settings.SWITCH_TIMEOUT,
|
||||||
ensp_mode=request.is_ensp
|
ensp_mode=request.is_ensp
|
||||||
)
|
)
|
||||||
@ -253,7 +220,7 @@ async def root():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ... 其他路由保持不变 ...
|
||||||
|
|
||||||
@router.get("/traffic/switch/interfaces", summary="获取交换机的网络接口")
|
@router.get("/traffic/switch/interfaces", summary="获取交换机的网络接口")
|
||||||
async def get_switch_interfaces(switch_ip: str):
|
async def get_switch_interfaces(switch_ip: str):
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import asyncssh
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||||
|
|
||||||
from src.backend.config import settings
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------
|
# ----------------------
|
||||||
# 数据模型
|
# 数据模型
|
||||||
@ -46,18 +44,18 @@ class SSHConnectionException(SwitchConfigException):
|
|||||||
class SwitchConfigurator:
|
class SwitchConfigurator:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
username: str = None,
|
username: str = "admin",
|
||||||
password: str = None,
|
password: str = "admin",
|
||||||
timeout: int = None,
|
timeout: int = 10,
|
||||||
max_workers: int = 5,
|
max_workers: int = 5,
|
||||||
ensp_mode: bool = False,
|
ensp_mode: bool = False,
|
||||||
ensp_port: int = 2000,
|
ensp_port: int = 2000,
|
||||||
ensp_command_delay: float = 0.5,
|
ensp_command_delay: float = 0.5,
|
||||||
**ssh_options
|
**ssh_options
|
||||||
):
|
):
|
||||||
self.username = username if username is not None else settings.SWITCH_USERNAME
|
self.username = username
|
||||||
self.password = password if password is not None else settings.SWITCH_PASSWORD
|
self.password = password
|
||||||
self.timeout = timeout if timeout is not None else settings.SWITCH_TIMEOUT
|
self.timeout = timeout
|
||||||
self.semaphore = asyncio.Semaphore(max_workers)
|
self.semaphore = asyncio.Semaphore(max_workers)
|
||||||
self.backup_dir = Path("config_backups")
|
self.backup_dir = Path("config_backups")
|
||||||
self.backup_dir.mkdir(exist_ok=True)
|
self.backup_dir.mkdir(exist_ok=True)
|
||||||
|
|||||||
@ -21,7 +21,7 @@ class NetworkScanner:
|
|||||||
# 扫描开放22(SSH)或23(Telnet)端口的设备
|
# 扫描开放22(SSH)或23(Telnet)端口的设备
|
||||||
self.nm.scan(
|
self.nm.scan(
|
||||||
hosts=subnet,
|
hosts=subnet,
|
||||||
arguments="-p 22,23,80,161 --min-rate 1000 --max-retries 1"
|
arguments="-p 22,23 --open -T4"
|
||||||
)
|
)
|
||||||
|
|
||||||
devices = []
|
devices = []
|
||||||
|
|||||||
@ -16,8 +16,6 @@ class Settings(BaseSettings):
|
|||||||
SILICONFLOW_API_URL: str = os.getenv("SILICONFLOW_API_URL", "https://api.siliconflow.cn/v1")
|
SILICONFLOW_API_URL: str = os.getenv("SILICONFLOW_API_URL", "https://api.siliconflow.cn/v1")
|
||||||
|
|
||||||
# 交换机配置
|
# 交换机配置
|
||||||
SWITCH_USERNAME: str = os.getenv("SWITCH_USERNAME", "admin")
|
|
||||||
SWITCH_PASSWORD: str = os.getenv("SWITCH_PASSWORD", "admin")
|
|
||||||
SWITCH_TIMEOUT: int = os.getenv("SWITCH_TIMEOUT", 10)
|
SWITCH_TIMEOUT: int = os.getenv("SWITCH_TIMEOUT", 10)
|
||||||
|
|
||||||
# eNSP配置
|
# eNSP配置
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
fastapi==0.110.0
|
fastapi==0.110.0
|
||||||
uvicorn==0.29.0
|
uvicorn==0.29.0
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
|
pysnmp
|
||||||
pydantic==2.6.4
|
pydantic==2.6.4
|
||||||
pydantic-settings==2.2.1
|
pydantic-settings==2.2.1
|
||||||
openai==1.93.2
|
openai==1.93.2
|
||||||
|
|||||||
@ -1,138 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Box,
|
|
||||||
Dialog,
|
|
||||||
DialogBackdrop,
|
|
||||||
DialogPositioner,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogBody,
|
|
||||||
DialogFooter,
|
|
||||||
Field,
|
|
||||||
Input,
|
|
||||||
Stack,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { FiCheck } from 'react-icons/fi';
|
|
||||||
import Notification from '@/libs/system/Notification';
|
|
||||||
|
|
||||||
const MotionBox = motion(Box);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设备配置弹窗
|
|
||||||
* @param isOpen 是否打开
|
|
||||||
* @param onClose 关闭弹窗
|
|
||||||
* @param onSave 保存修改
|
|
||||||
* @param device 当前设备
|
|
||||||
* @returns {JSX.Element}
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
const DeviceConfigModal = ({ isOpen, onClose, onSave, device }) => {
|
|
||||||
const [username, setUsername] = useState(device.username || '');
|
|
||||||
const [password, setPassword] = useState(device.password || '');
|
|
||||||
const [saved, setSaved] = useState(false);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
const updatedDevice = { ...device, username, password };
|
|
||||||
onSave(updatedDevice);
|
|
||||||
setSaved(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setSaved(false);
|
|
||||||
onClose();
|
|
||||||
}, 1200);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.Root open={isOpen} onClose={onClose}>
|
|
||||||
<DialogBackdrop />
|
|
||||||
<DialogPositioner>
|
|
||||||
<MotionBox
|
|
||||||
as={DialogContent}
|
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.8 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
bg={'whiteAlpha.100'}
|
|
||||||
backdropFilter={'blur(12px)'}
|
|
||||||
border={'1px solid'}
|
|
||||||
borderColor={'whiteAlpha.300'}
|
|
||||||
>
|
|
||||||
<DialogHeader>交换机设备配置</DialogHeader>
|
|
||||||
|
|
||||||
<DialogBody>
|
|
||||||
<Stack gap={4}>
|
|
||||||
<Field.Root>
|
|
||||||
<Field.Label>交换机用户名</Field.Label>
|
|
||||||
<Input
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
placeholder={'请输入设备用户名'}
|
|
||||||
bg={'whiteAlpha.200'}
|
|
||||||
/>
|
|
||||||
</Field.Root>
|
|
||||||
|
|
||||||
<Field.Root>
|
|
||||||
<Field.Label>交换机密码</Field.Label>
|
|
||||||
<Input
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
placeholder={'请输入设备密码'}
|
|
||||||
bg={'whiteAlpha.200'}
|
|
||||||
type={'password'}
|
|
||||||
/>
|
|
||||||
</Field.Root>
|
|
||||||
</Stack>
|
|
||||||
</DialogBody>
|
|
||||||
|
|
||||||
<DialogFooter justifyContent={'space-between'}>
|
|
||||||
<Button
|
|
||||||
variant={'outline'}
|
|
||||||
borderColor={'whiteAlpha.500'}
|
|
||||||
color={'white'}
|
|
||||||
onClick={onClose}
|
|
||||||
_hover={{ bg: 'rgba(0, 0, 255, 0.3)' }}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant={'outline'}
|
|
||||||
borderColor={'whiteAlpha.500'}
|
|
||||||
color={'white'}
|
|
||||||
onClick={handleSave}
|
|
||||||
isDisabled={saved}
|
|
||||||
width={'80px'}
|
|
||||||
position={'relative'}
|
|
||||||
_hover={{ bg: 'rgba(0, 0, 255, 0.3)' }}
|
|
||||||
>
|
|
||||||
{saved ? (
|
|
||||||
<motion.div
|
|
||||||
key={'saved'}
|
|
||||||
initial={{ opacity: 0, scale: 0.5 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.5 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
>
|
|
||||||
<FiCheck size={20} color={'lightgreen'} />
|
|
||||||
</motion.div>
|
|
||||||
) : (
|
|
||||||
<motion.div
|
|
||||||
key={'save'}
|
|
||||||
initial={{ opacity: 0, scale: 0.5 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.5 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</MotionBox>
|
|
||||||
</DialogPositioner>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeviceConfigModal;
|
|
||||||
@ -34,6 +34,7 @@ const ConfigPage = () => {
|
|||||||
const [editableConfig, setEditableConfig] = useState('');
|
const [editableConfig, setEditableConfig] = useState('');
|
||||||
const [applying, setApplying] = useState(false);
|
const [applying, setApplying] = useState(false);
|
||||||
const [hasParsed, setHasParsed] = useState(false);
|
const [hasParsed, setHasParsed] = useState(false);
|
||||||
|
const [conmmand, setConmmand] = useState([]);
|
||||||
const [isPeizhi, setisPeizhi] = useState(false);
|
const [isPeizhi, setisPeizhi] = useState(false);
|
||||||
const [isApplying, setIsApplying] = useState(false);
|
const [isApplying, setIsApplying] = useState(false);
|
||||||
const [applyStatus, setApplyStatus] = useState([]);
|
const [applyStatus, setApplyStatus] = useState([]);
|
||||||
@ -60,6 +61,13 @@ const ConfigPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
/**
|
||||||
|
const response = await api.parseCommand(inputText);
|
||||||
|
if (response) {
|
||||||
|
setParsedConfig(response);
|
||||||
|
setEditableConfig(response);
|
||||||
|
setHasParsed(true);
|
||||||
|
}**/
|
||||||
const performParse = async () => {
|
const performParse = async () => {
|
||||||
if (testMode) {
|
if (testMode) {
|
||||||
await Common.sleep(800 + Math.random() * 700);
|
await Common.sleep(800 + Math.random() * 700);
|
||||||
@ -89,6 +97,10 @@ const ConfigPage = () => {
|
|||||||
setParsedConfig(JSON.stringify(result.data));
|
setParsedConfig(JSON.stringify(result.data));
|
||||||
setEditableConfig(JSON.stringify(result.data));
|
setEditableConfig(JSON.stringify(result.data));
|
||||||
setHasParsed(true);
|
setHasParsed(true);
|
||||||
|
result = result.data;
|
||||||
|
if (result.config && Array.isArray(result.config.commands)) {
|
||||||
|
setConmmand(result.config.commands);
|
||||||
|
}
|
||||||
setisPeizhi(true);
|
setisPeizhi(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -100,6 +112,37 @@ const ConfigPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyCommand = async (switch_ip, command, index) => {
|
||||||
|
try {
|
||||||
|
setApplyStatus((prevStatus) => {
|
||||||
|
const updated = [...prevStatus];
|
||||||
|
updated[index] = 'in-progress';
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
const applyResult = testMode
|
||||||
|
? await Common.sleep(1000)
|
||||||
|
: await api.applyConfig(switch_ip, [command]);
|
||||||
|
|
||||||
|
if (applyResult?.data?.success) {
|
||||||
|
setApplyStatus((prevStatus) => {
|
||||||
|
const updated = [...prevStatus];
|
||||||
|
updated[index] = 'success';
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Notification.error({ title: '命令应用失败', description: '请检查命令是否合法' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setApplyStatus((prevStatus) => {
|
||||||
|
const updated = [...prevStatus];
|
||||||
|
updated[index] = 'failed';
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleApply = async () => {
|
const handleApply = async () => {
|
||||||
if (!editableConfig) {
|
if (!editableConfig) {
|
||||||
Notification.warn({
|
Notification.warn({
|
||||||
@ -110,11 +153,10 @@ const ConfigPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setApplying(true);
|
setApplying(true);
|
||||||
setIsApplying(true);
|
|
||||||
try {
|
try {
|
||||||
const applyOperation = testMode
|
const applyOperation = testMode
|
||||||
? Common.sleep(1000).then(() => ({ success: true }))
|
? Common.sleep(1000).then(() => ({ success: true }))
|
||||||
: await api.applyConfig(selectedDevice, JSON.parse(editableConfig)?.config?.commands);
|
: await api.applyConfig(selectedDevice, conmmand);
|
||||||
|
|
||||||
await Notification.promise({
|
await Notification.promise({
|
||||||
promise: applyOperation,
|
promise: applyOperation,
|
||||||
@ -145,6 +187,7 @@ const ConfigPage = () => {
|
|||||||
<Heading fontSize={'xl'} color={'teal.300'}>
|
<Heading fontSize={'xl'} color={'teal.300'}>
|
||||||
交换机配置中心
|
交换机配置中心
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Field.Root>
|
<Field.Root>
|
||||||
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
|
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
|
||||||
选择交换机设备
|
选择交换机设备
|
||||||
@ -180,21 +223,21 @@ const ConfigPage = () => {
|
|||||||
</Portal>
|
</Portal>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
</Field.Root>
|
</Field.Root>
|
||||||
|
|
||||||
<Field.Root>
|
<Field.Root>
|
||||||
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
|
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
|
||||||
配置指令输入
|
配置指令输入
|
||||||
</Field.Label>
|
</Field.Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder={'例:创建VLAN 10并配置IP 192.168.10.1/24并在端口1启用SSH访问"'}
|
placeholder={'例子:创建VLAN 10并配置IP 192.168.10.1/24并在端口1启用SSH访问"'}
|
||||||
value={inputText}
|
value={inputText}
|
||||||
colorPalette={'teal'}
|
|
||||||
orientation={'vertical'}
|
|
||||||
onChange={(e) => setInputText(e.target.value)}
|
onChange={(e) => setInputText(e.target.value)}
|
||||||
bg={'whiteAlpha.200'}
|
bg={'whiteAlpha.200'}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
/>
|
/>
|
||||||
</Field.Root>
|
</Field.Root>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
colorScheme={'teal'}
|
colorScheme={'teal'}
|
||||||
variant={'solid'}
|
variant={'solid'}
|
||||||
@ -204,6 +247,57 @@ const ConfigPage = () => {
|
|||||||
>
|
>
|
||||||
解析配置
|
解析配置
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{hasParsed && (
|
||||||
|
<FadeInWrapper>
|
||||||
|
<Box
|
||||||
|
p={4}
|
||||||
|
bg={'whiteAlpha.100'}
|
||||||
|
borderRadius={'xl'}
|
||||||
|
border={'1px solid'}
|
||||||
|
borderColor={'whiteAlpha.300'}
|
||||||
|
>
|
||||||
|
<Text fontWeight={'bold'} mb={2} fontSize="sm">
|
||||||
|
生成配置:
|
||||||
|
</Text>
|
||||||
|
<Textarea
|
||||||
|
value={JSON.stringify(editableConfig)}
|
||||||
|
rows={12}
|
||||||
|
onChange={(e) => setEditableConfig(e.target.value)}
|
||||||
|
fontFamily={'monospace'}
|
||||||
|
size={'sm'}
|
||||||
|
bg={'blackAlpha.200'}
|
||||||
|
whiteSpace="pre-wrap"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HStack mt={4} spacing={3} justify={'flex-end'}>
|
||||||
|
<Button
|
||||||
|
variant={'outline'}
|
||||||
|
colorScheme={'gray'}
|
||||||
|
size={'sm'}
|
||||||
|
onClick={() => {
|
||||||
|
setEditableConfig(parsedConfig);
|
||||||
|
Notification.success({
|
||||||
|
title: '成功重置配置!',
|
||||||
|
description: '现在您可以重新审查生成的配置',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重置为原始配置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorScheme={'teal'}
|
||||||
|
size={'sm'}
|
||||||
|
onClick={handleApply}
|
||||||
|
isLoading={applying}
|
||||||
|
isDisabled={!editableConfig}
|
||||||
|
>
|
||||||
|
应用到交换机
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</FadeInWrapper>
|
||||||
|
)}
|
||||||
{isPeizhi && parsedConfig && (
|
{isPeizhi && parsedConfig && (
|
||||||
<FadeInWrapper delay={0.2}>
|
<FadeInWrapper delay={0.2}>
|
||||||
<VStack spacing={4} align={'stretch'}>
|
<VStack spacing={4} align={'stretch'}>
|
||||||
@ -212,7 +306,7 @@ const ConfigPage = () => {
|
|||||||
try {
|
try {
|
||||||
parsed = JSON.parse(editableConfig);
|
parsed = JSON.parse(editableConfig);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return <Text color={'red.300'}>配置 JSON 格式错误,无法解析</Text>;
|
return <Text color={'red.300'}>配置 JSON 格式错误,无法解析。</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = parsed.config ? [parsed.config] : parsed;
|
const config = parsed.config ? [parsed.config] : parsed;
|
||||||
@ -225,7 +319,7 @@ const ConfigPage = () => {
|
|||||||
border={'1px solid'}
|
border={'1px solid'}
|
||||||
borderColor={'whiteAlpha.300'}
|
borderColor={'whiteAlpha.300'}
|
||||||
>
|
>
|
||||||
<Text fontSize={'lg'} fontWeight={'bold'} mb={2}>
|
<Text fontSize="lg" fontWeight="bold" mb={2}>
|
||||||
配置类型: {cfg.type}
|
配置类型: {cfg.type}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@ -249,6 +343,7 @@ const ConfigPage = () => {
|
|||||||
setEditableConfig(JSON.stringify(updated, null, 2));
|
setEditableConfig(JSON.stringify(updated, null, 2));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Field.HelperText>可编辑字段: {key}</Field.HelperText>
|
||||||
</Field.Root>
|
</Field.Root>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -258,7 +353,7 @@ const ConfigPage = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
{cfg.commands?.map((cmd, i) => (
|
{cfg.commands?.map((cmd, i) => (
|
||||||
<Field.Root key={i} colorPalette={'teal'} orientation={'vertical'} mb={2}>
|
<Field.Root key={i} colorPalette={'teal'} orientation={'vertical'} mb={2}>
|
||||||
<Field.Label fontSize="sm">命令 {i + 1}</Field.Label>
|
<Field.Label fontSize="sm">命令 #{i + 1}</Field.Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
fontFamily={'monospace'}
|
fontFamily={'monospace'}
|
||||||
@ -272,51 +367,24 @@ const ConfigPage = () => {
|
|||||||
/>
|
/>
|
||||||
</Field.Root>
|
</Field.Root>
|
||||||
))}
|
))}
|
||||||
<HStack mt={4} spacing={3} justify={'flex-end'}>
|
|
||||||
<Button
|
|
||||||
variant={'outline'}
|
|
||||||
colorScheme={'gray'}
|
|
||||||
size={'sm'}
|
|
||||||
onClick={() => {
|
|
||||||
setEditableConfig(parsedConfig);
|
|
||||||
Notification.success({
|
|
||||||
title: '成功重置配置!',
|
|
||||||
description: '现在您可以重新审查生成的配置',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
重置为原始配置
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
variant={'outline'}
|
mt={3}
|
||||||
colorScheme={'gray'}
|
colorScheme={'teal'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Notification.success({
|
Notification.success({
|
||||||
title: `配置 ${cfg.type} 已保存`,
|
title: `配置 ${cfg.type} 已保存`,
|
||||||
description: '修改已同步至内存配置',
|
description: '修改已同步至内存配置',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
保存当前配置
|
保存当前配置
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
|
||||||
colorScheme={'teal'}
|
|
||||||
size={'sm'}
|
|
||||||
onClick={handleApply}
|
|
||||||
isLoading={applying}
|
|
||||||
isDisabled={!editableConfig}
|
|
||||||
>
|
|
||||||
应用到交换机
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
</Box>
|
||||||
));
|
));
|
||||||
})()}
|
})()}
|
||||||
|
{isApplying && (
|
||||||
{
|
|
||||||
<FadeInWrapper delay={0.2}>
|
<FadeInWrapper delay={0.2}>
|
||||||
<VStack spacing={4} align={'stretch'}>
|
<VStack spacing={4} align={'stretch'}>
|
||||||
<Box
|
<Box
|
||||||
@ -330,7 +398,7 @@ const ConfigPage = () => {
|
|||||||
应用配置命令
|
应用配置命令
|
||||||
</Text>
|
</Text>
|
||||||
<Box>
|
<Box>
|
||||||
{JSON.parse(editableConfig).config?.commands.map((command, index) => (
|
{conmmand.map((command, index) => (
|
||||||
<HStack key={index} mb={2}>
|
<HStack key={index} mb={2}>
|
||||||
<Text fontSize={'sm'} flex={1}>
|
<Text fontSize={'sm'} flex={1}>
|
||||||
{command}
|
{command}
|
||||||
@ -363,7 +431,7 @@ const ConfigPage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</VStack>
|
</VStack>
|
||||||
</FadeInWrapper>
|
</FadeInWrapper>
|
||||||
}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
</FadeInWrapper>
|
</FadeInWrapper>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import ConfigTool from '@/libs/config/ConfigTool';
|
|||||||
import Common from '@/libs/common';
|
import Common from '@/libs/common';
|
||||||
import switchIcon from '@/resources/icon/pages/devices/switch.png';
|
import switchIcon from '@/resources/icon/pages/devices/switch.png';
|
||||||
import Notification from '@/libs/system/Notification';
|
import Notification from '@/libs/system/Notification';
|
||||||
import DeviceConfigModal from '@/components/pages/config/DeviceConfigModal';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 交换机管理
|
* 交换机管理
|
||||||
@ -30,8 +29,6 @@ const DevicesPage = () => {
|
|||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [editingIndex, setEditingIndex] = useState(null);
|
const [editingIndex, setEditingIndex] = useState(null);
|
||||||
const [editingName, setEditingName] = useState('');
|
const [editingName, setEditingName] = useState('');
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [currentDevice, setCurrentDevice] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const config = ConfigTool.load();
|
const config = ConfigTool.load();
|
||||||
@ -56,23 +53,6 @@ const DevicesPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenConfigModal = (device) => {
|
|
||||||
setCurrentDevice(device);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveDeviceConfig = (updatedDevice) => {
|
|
||||||
const updatedDevices = devices.map((device) =>
|
|
||||||
device.ip === updatedDevice.ip ? updatedDevice : device
|
|
||||||
);
|
|
||||||
setDevices(updatedDevices);
|
|
||||||
ConfigTool.save({ ...ConfigTool.load(), devices: updatedDevices });
|
|
||||||
Notification.success({
|
|
||||||
title: '设备配置已保存!',
|
|
||||||
});
|
|
||||||
setIsModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentTitle title={'交换机设备'}>
|
<DocumentTitle title={'交换机设备'}>
|
||||||
<DashboardBackground />
|
<DashboardBackground />
|
||||||
@ -144,15 +124,6 @@ const DevicesPage = () => {
|
|||||||
{'端口: '}
|
{'端口: '}
|
||||||
{device.ports.join(', ')}
|
{device.ports.join(', ')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Button
|
|
||||||
size={'sm'}
|
|
||||||
colorPalette={'teal'}
|
|
||||||
mt={2}
|
|
||||||
onClick={() => handleOpenConfigModal(device)}
|
|
||||||
>
|
|
||||||
配置
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@ -163,15 +134,6 @@ const DevicesPage = () => {
|
|||||||
</VStack>
|
</VStack>
|
||||||
</FadeInWrapper>
|
</FadeInWrapper>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|
||||||
{isModalOpen && currentDevice && (
|
|
||||||
<DeviceConfigModal
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
onClose={() => setIsModalOpen(false)}
|
|
||||||
onSave={handleSaveDeviceConfig}
|
|
||||||
device={currentDevice}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user