import React, { useState, useEffect, useRef } from 'react'; import { Modal, Form, Input, InputNumber, Button, message, Checkbox, Divider, Select, Alert, Card, Row, Col, Typography, Collapse } from 'antd'; import { DatabaseOutlined, ConsoleSqlOutlined, FileTextOutlined, CloudServerOutlined, AppstoreAddOutlined, CloudOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { DBGetDatabases, TestConnection, RedisConnect } from '../../wailsjs/go/app/App'; import { SavedConnection } from '../types'; const { Meta } = Card; const { Text } = Typography; const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialValues?: SavedConnection | null }> = ({ open, onClose, initialValues }) => { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const [useSSH, setUseSSH] = useState(false); const [dbType, setDbType] = useState('mysql'); const [step, setStep] = useState(1); // 1: Select Type, 2: Configure const [activeGroup, setActiveGroup] = useState(0); // Active category index in step 1 const [testResult, setTestResult] = useState<{ type: 'success' | 'error', message: string } | null>(null); const [dbList, setDbList] = useState([]); const [redisDbList, setRedisDbList] = useState([]); // Redis databases 0-15 const testInFlightRef = useRef(false); const testTimerRef = useRef(null); const addConnection = useStore((state) => state.addConnection); const updateConnection = useStore((state) => state.updateConnection); useEffect(() => { if (open) { setTestResult(null); // Reset test result setDbList([]); setRedisDbList([]); if (initialValues) { // Edit mode: Go directly to step 2 setStep(2); form.setFieldsValue({ type: initialValues.config.type, name: initialValues.name, host: initialValues.config.host, port: initialValues.config.port, user: initialValues.config.user, password: initialValues.config.password, database: initialValues.config.database, includeDatabases: initialValues.includeDatabases, includeRedisDatabases: initialValues.includeRedisDatabases, useSSH: initialValues.config.useSSH, sshHost: initialValues.config.ssh?.host, sshPort: initialValues.config.ssh?.port, sshUser: initialValues.config.ssh?.user, sshPassword: initialValues.config.ssh?.password, sshKeyPath: initialValues.config.ssh?.keyPath, driver: (initialValues.config as any).driver, dsn: (initialValues.config as any).dsn, timeout: (initialValues.config as any).timeout || 30 }); setUseSSH(initialValues.config.useSSH || false); setDbType(initialValues.config.type); // 如果是 Redis 编辑模式,设置已保存的 Redis 数据库列表 if (initialValues.config.type === 'redis') { setRedisDbList(Array.from({ length: 16 }, (_, i) => i)); } } else { // Create mode: Start at step 1 setStep(1); form.resetFields(); setUseSSH(false); setDbType('mysql'); setActiveGroup(0); } } }, [open, initialValues]); useEffect(() => { return () => { if (testTimerRef.current !== null) { window.clearTimeout(testTimerRef.current); testTimerRef.current = null; } }; }, []); const handleOk = async () => { try { const values = await form.validateFields(); setLoading(true); const config = await buildConfig(values); const isRedisType = values.type === 'redis'; const newConn = { id: initialValues ? initialValues.id : Date.now().toString(), name: values.name || (values.type === 'sqlite' ? 'SQLite DB' : (values.type === 'redis' ? `Redis ${values.host}` : values.host)), config: config, includeDatabases: values.includeDatabases, includeRedisDatabases: isRedisType ? values.includeRedisDatabases : undefined }; if (initialValues) { updateConnection(newConn); message.success('配置已更新(未连接)'); } else { addConnection(newConn); message.success('配置已保存(未连接)'); } setLoading(false); form.resetFields(); setUseSSH(false); setDbType('mysql'); setStep(1); onClose(); } catch (e) { setLoading(false); } }; const requestTest = () => { if (loading) return; if (testTimerRef.current !== null) return; testTimerRef.current = window.setTimeout(() => { testTimerRef.current = null; handleTest(); }, 0); }; const handleTest = async () => { if (testInFlightRef.current) return; testInFlightRef.current = true; try { const values = await form.validateFields(); setLoading(true); setTestResult(null); const config = await buildConfig(values); // Use different API for Redis const isRedisType = values.type === 'redis'; const res = isRedisType ? await RedisConnect(config as any) : await TestConnection(config as any); if (res.success) { setTestResult({ type: 'success', message: res.message }); if (isRedisType) { // Redis: generate database list 0-15 setRedisDbList(Array.from({ length: 16 }, (_, i) => i)); } else { // Other databases: fetch database list const dbRes = await DBGetDatabases(config as any); if (dbRes.success) { const dbs = (dbRes.data as any[]).map((row: any) => row.Database || row.database); setDbList(dbs); } } } else { setTestResult({ type: 'error', message: "测试失败: " + res.message }); } } catch (e) { // ignore } finally { testInFlightRef.current = false; setLoading(false); } }; const buildConfig = async (values: any) => { const sshConfig = values.useSSH ? { host: values.sshHost, port: Number(values.sshPort), user: values.sshUser, password: values.sshPassword || "", keyPath: values.sshKeyPath || "" } : { host: "", port: 22, user: "", password: "", keyPath: "" }; return { type: values.type, host: values.host || "", port: Number(values.port || 0), user: values.user || "", password: values.password || "", database: values.database || "", useSSH: !!values.useSSH, ssh: sshConfig, driver: values.driver, dsn: values.dsn, timeout: Number(values.timeout || 30) }; }; const handleTypeSelect = (type: string) => { setDbType(type); form.setFieldsValue({ type: type }); // Auto-fill default port let defaultPort = 3306; switch (type) { case 'mysql': defaultPort = 3306; break; case 'postgres': defaultPort = 5432; break; case 'redis': defaultPort = 6379; break; case 'oracle': defaultPort = 1521; break; case 'dameng': defaultPort = 5236; break; case 'kingbase': defaultPort = 54321; break; case 'sqlserver': defaultPort = 1433; break; case 'mongodb': defaultPort = 27017; break; case 'highgo': defaultPort = 5866; break; case 'mariadb': defaultPort = 3306; break; case 'vastbase': defaultPort = 5432; break; default: defaultPort = 3306; } if (type !== 'sqlite' && type !== 'custom') { form.setFieldsValue({ port: defaultPort }); } setStep(2); }; const isSqlite = dbType === 'sqlite'; const isCustom = dbType === 'custom'; const isRedis = dbType === 'redis'; const dbTypeGroups = [ { label: '关系型数据库', items: [ { key: 'mysql', name: 'MySQL', icon: }, { key: 'mariadb', name: 'MariaDB', icon: }, { key: 'postgres', name: 'PostgreSQL', icon: }, { key: 'sqlserver', name: 'SQL Server', icon: }, { key: 'sqlite', name: 'SQLite', icon: }, { key: 'oracle', name: 'Oracle', icon: }, ]}, { label: '国产数据库', items: [ { key: 'dameng', name: 'Dameng (达梦)', icon: }, { key: 'kingbase', name: 'Kingbase (人大金仓)', icon: }, { key: 'highgo', name: 'HighGo (瀚高)', icon: }, { key: 'vastbase', name: 'Vastbase (海量)', icon: }, ]}, { label: 'NoSQL', items: [ { key: 'mongodb', name: 'MongoDB', icon: }, { key: 'redis', name: 'Redis', icon: }, ]}, { label: '其他', items: [ { key: 'custom', name: 'Custom (自定义)', icon: }, ]}, ]; const dbTypes = dbTypeGroups.flatMap(g => g.items); const renderStep1 = () => (
{/* 左侧分类导航 */}
{dbTypeGroups.map((group, idx) => (
setActiveGroup(idx)} style={{ padding: '10px 12px', cursor: 'pointer', borderRadius: 6, marginBottom: 4, background: activeGroup === idx ? '#e6f4ff' : 'transparent', color: activeGroup === idx ? '#1677ff' : undefined, fontWeight: activeGroup === idx ? 500 : 400, transition: 'all 0.2s', fontSize: 13, }} > {group.label}
))}
{/* 右侧数据源卡片 */}
{dbTypeGroups[activeGroup]?.items.map(item => ( handleTypeSelect(item.key)} style={{ textAlign: 'center', cursor: 'pointer', height: 100 }} styles={{ body: { padding: '16px 8px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%' } }} >
{item.icon}
{item.name}
))}
); const renderStep2 = () => (
{ if (testResult) setTestResult(null); // Clear result on change if (changed.useSSH !== undefined) setUseSSH(changed.useSSH); // Type change handled by step 1, but keep sync if select changes (hidden now) if (changed.type !== undefined) setDbType(changed.type); }} > {/* Hidden Type Field to keep form value synced */} {isCustom ? ( <> ) : ( <>
{!isSqlite && ( )}
{/* Redis specific: password only, no username */} {isRedis && ( <> )} {/* Non-Redis, non-SQLite: username and password */} {!isSqlite && !isRedis && (
)} {!isSqlite && !isRedis && ( )} {!isSqlite && ( <> 使用 SSH 隧道 (SSH Tunnel) {useSSH && (
)} ) }]} /> )} )} {testResult && ( )} ); const getFooter = () => { if (step === 1) { return [ ]; } return [ !initialValues && , , , ]; }; const getTitle = () => { if (step === 1) return "选择数据源类型"; const typeName = dbTypes.find(t => t.key === dbType)?.name || dbType; return initialValues ? "编辑连接" : `新建 ${typeName} 连接`; }; return ( {step === 1 ? renderStep1() : renderStep2()} ); }; export default ConnectionModal;