import React, { useState, useEffect, useRef } from 'react'; import { Modal, Form, Input, InputNumber, Button, message, Checkbox, Divider, Select, Alert, Card, Row, Col, Typography, Collapse, Space, Table, Tag } from 'antd'; import { DatabaseOutlined, ConsoleSqlOutlined, FileTextOutlined, CloudServerOutlined, AppstoreAddOutlined, CloudOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { DBGetDatabases, MongoDiscoverMembers, TestConnection, RedisConnect } from '../../wailsjs/go/app/App'; import { MongoMemberInfo, SavedConnection } from '../types'; const { Meta } = Card; const { Text } = Typography; const getDefaultPortByType = (type: string) => { switch (type) { case 'mysql': return 3306; case 'sphinx': return 9306; case 'postgres': return 5432; case 'redis': return 6379; case 'tdengine': return 6041; case 'oracle': return 1521; case 'dameng': return 5236; case 'kingbase': return 54321; case 'sqlserver': return 1433; case 'mongodb': return 27017; case 'highgo': return 5866; case 'mariadb': return 3306; case 'vastbase': return 5432; default: return 3306; } }; 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 [mongoMembers, setMongoMembers] = useState([]); const [discoveringMembers, setDiscoveringMembers] = useState(false); const testInFlightRef = useRef(false); const testTimerRef = useRef(null); const addConnection = useStore((state) => state.addConnection); const updateConnection = useStore((state) => state.updateConnection); const mysqlTopology = Form.useWatch('mysqlTopology', form) || 'single'; const mongoTopology = Form.useWatch('mongoTopology', form) || 'single'; const mongoSrv = Form.useWatch('mongoSrv', form) || false; const parseHostPort = (raw: string, defaultPort: number): { host: string; port: number } | null => { const text = String(raw || '').trim(); if (!text) { return null; } if (text.startsWith('[')) { const closingBracket = text.indexOf(']'); if (closingBracket > 0) { const host = text.slice(1, closingBracket).trim(); const portText = text.slice(closingBracket + 1).trim().replace(/^:/, ''); const parsedPort = Number(portText); return { host: host || 'localhost', port: Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : defaultPort, }; } } const colonCount = (text.match(/:/g) || []).length; if (colonCount === 1) { const splitIndex = text.lastIndexOf(':'); const host = text.slice(0, splitIndex).trim(); const portText = text.slice(splitIndex + 1).trim(); const parsedPort = Number(portText); return { host: host || 'localhost', port: Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : defaultPort, }; } return { host: text, port: defaultPort }; }; const toAddress = (host: string, port: number, defaultPort: number) => { const safeHost = String(host || '').trim() || 'localhost'; const safePort = Number.isFinite(Number(port)) && Number(port) > 0 ? Number(port) : defaultPort; return `${safeHost}:${safePort}`; }; const normalizeAddressList = (rawList: unknown, defaultPort: number): string[] => { const list = Array.isArray(rawList) ? rawList : []; const seen = new Set(); const result: string[] = []; list.forEach((entry) => { const parsed = parseHostPort(String(entry || ''), defaultPort); if (!parsed) { return; } const normalized = toAddress(parsed.host, parsed.port, defaultPort); if (seen.has(normalized)) { return; } seen.add(normalized); result.push(normalized); }); return result; }; const normalizeMongoSrvHostList = (rawList: unknown, defaultPort: number): string[] => { const list = Array.isArray(rawList) ? rawList : []; const seen = new Set(); const result: string[] = []; list.forEach((entry) => { const parsed = parseHostPort(String(entry || ''), defaultPort); if (!parsed?.host) { return; } const host = String(parsed.host).trim(); if (!host || seen.has(host)) { return; } seen.add(host); result.push(host); }); return result; }; const safeDecode = (text: string) => { try { return decodeURIComponent(text); } catch { return text; } }; const parseMultiHostUri = (uriText: string, expectedScheme: string) => { const prefix = `${expectedScheme}://`; if (!uriText.toLowerCase().startsWith(prefix)) { return null; } let rest = uriText.slice(prefix.length); let queryText = ''; const queryIndex = rest.indexOf('?'); if (queryIndex >= 0) { queryText = rest.slice(queryIndex + 1); rest = rest.slice(0, queryIndex); } let pathText = ''; const slashIndex = rest.indexOf('/'); if (slashIndex >= 0) { pathText = rest.slice(slashIndex + 1); rest = rest.slice(0, slashIndex); } let hostText = rest; let username = ''; let password = ''; const atIndex = rest.lastIndexOf('@'); if (atIndex >= 0) { const userInfo = rest.slice(0, atIndex); hostText = rest.slice(atIndex + 1); const colonIndex = userInfo.indexOf(':'); if (colonIndex >= 0) { username = safeDecode(userInfo.slice(0, colonIndex)); password = safeDecode(userInfo.slice(colonIndex + 1)); } else { username = safeDecode(userInfo); } } const hosts = hostText .split(',') .map((item) => item.trim()) .filter(Boolean); return { username, password, hosts, database: safeDecode(pathText), params: new URLSearchParams(queryText), }; }; const parseUriToValues = (uriText: string, type: string): Record | null => { const trimmedUri = String(uriText || '').trim(); if (!trimmedUri) { return null; } if (type === 'mysql' || type === 'mariadb' || type === 'sphinx') { const mysqlDefaultPort = getDefaultPortByType(type); const parsed = parseMultiHostUri(trimmedUri, 'mysql'); if (!parsed) { return null; } const hostList = normalizeAddressList(parsed.hosts, mysqlDefaultPort); const primary = parseHostPort(hostList[0] || `localhost:${mysqlDefaultPort}`, mysqlDefaultPort); const timeoutValue = Number(parsed.params.get('timeout')); return { host: primary?.host || 'localhost', port: primary?.port || mysqlDefaultPort, user: parsed.username, password: parsed.password, database: parsed.database || '', mysqlTopology: hostList.length > 1 || parsed.params.get('topology') === 'replica' ? 'replica' : 'single', mysqlReplicaHosts: hostList.slice(1), timeout: Number.isFinite(timeoutValue) && timeoutValue > 0 ? timeoutValue : undefined, }; } if (type === 'mongodb') { const parsed = parseMultiHostUri(trimmedUri, 'mongodb') || parseMultiHostUri(trimmedUri, 'mongodb+srv'); if (!parsed) { return null; } const isSrv = trimmedUri.toLowerCase().startsWith('mongodb+srv://'); const hostList = isSrv ? normalizeMongoSrvHostList(parsed.hosts, 27017) : normalizeAddressList(parsed.hosts, 27017); const primary = isSrv ? { host: hostList[0] || 'localhost', port: 27017 } : parseHostPort(hostList[0] || 'localhost:27017', 27017); const timeoutMs = Number(parsed.params.get('connectTimeoutMS') || parsed.params.get('serverSelectionTimeoutMS')); return { host: primary?.host || 'localhost', port: primary?.port || 27017, user: parsed.username, password: parsed.password, database: parsed.database || '', mongoTopology: hostList.length > 1 || !!parsed.params.get('replicaSet') ? 'replica' : 'single', mongoHosts: hostList.slice(1), mongoSrv: isSrv, mongoReplicaSet: parsed.params.get('replicaSet') || '', mongoAuthSource: parsed.params.get('authSource') || '', mongoReadPreference: parsed.params.get('readPreference') || 'primary', mongoAuthMechanism: parsed.params.get('authMechanism') || '', timeout: Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.ceil(timeoutMs / 1000) : undefined, savePassword: true, }; } return null; }; const createUriAwareRequiredRule = ( messageText: string, validateValue?: (value: unknown) => boolean ) => ({ getFieldValue }: { getFieldValue: (name: string) => unknown }) => ({ validator(_: unknown, value: unknown) { const uriText = String(getFieldValue('uri') || '').trim(); const type = String(getFieldValue('type') || dbType).trim().toLowerCase(); if (uriText && parseUriToValues(uriText, type)) { return Promise.resolve(); } const valid = validateValue ? validateValue(value) : String(value ?? '').trim() !== ''; return valid ? Promise.resolve() : Promise.reject(new Error(messageText)); } }); const getUriPlaceholder = () => { if (dbType === 'mysql' || dbType === 'mariadb' || dbType === 'sphinx') { const defaultPort = getDefaultPortByType(dbType); return `mysql://user:pass@127.0.0.1:${defaultPort},127.0.0.2:${defaultPort}/db_name?topology=replica`; } if (dbType === 'mongodb') { return 'mongodb+srv://user:pass@cluster0.example.com/db_name?authSource=admin&authMechanism=SCRAM-SHA-256'; } return '例如: postgres://user:pass@127.0.0.1:5432/db_name'; }; const buildUriFromValues = (values: any) => { const type = String(values.type || '').trim().toLowerCase(); const defaultPort = getDefaultPortByType(type); const host = String(values.host || 'localhost').trim(); const port = Number(values.port || defaultPort); const user = String(values.user || '').trim(); const password = String(values.password || ''); const database = String(values.database || '').trim(); const timeout = Number(values.timeout || 30); const encodedAuth = user ? `${encodeURIComponent(user)}${password ? `:${encodeURIComponent(password)}` : ''}@` : ''; if (type === 'mysql' || type === 'mariadb' || type === 'sphinx') { const primary = toAddress(host, port, defaultPort); const replicas = values.mysqlTopology === 'replica' ? normalizeAddressList(values.mysqlReplicaHosts, defaultPort) : []; const hosts = normalizeAddressList([primary, ...replicas], defaultPort); const params = new URLSearchParams(); if (hosts.length > 1 || values.mysqlTopology === 'replica') { params.set('topology', 'replica'); } if (Number.isFinite(timeout) && timeout > 0) { params.set('timeout', String(timeout)); } const dbPath = database ? `/${encodeURIComponent(database)}` : '/'; const query = params.toString(); return `mysql://${encodedAuth}${hosts.join(',')}${dbPath}${query ? `?${query}` : ''}`; } if (type === 'mongodb') { const useSrv = !!values.mongoSrv; const primaryAddress = useSrv ? (parseHostPort(host, 27017)?.host || host || 'localhost') : toAddress(host, port, 27017); const extraNodes = values.mongoTopology === 'replica' ? (useSrv ? normalizeMongoSrvHostList(values.mongoHosts, 27017) : normalizeAddressList(values.mongoHosts, 27017)) : []; const hosts = useSrv ? normalizeMongoSrvHostList([primaryAddress, ...extraNodes], 27017) : normalizeAddressList([primaryAddress, ...extraNodes], 27017); const scheme = useSrv ? 'mongodb+srv' : 'mongodb'; const params = new URLSearchParams(); const authSource = String(values.mongoAuthSource || database || 'admin').trim(); if (authSource) { params.set('authSource', authSource); } const replicaSet = String(values.mongoReplicaSet || '').trim(); if (replicaSet) { params.set('replicaSet', replicaSet); } const readPreference = String(values.mongoReadPreference || '').trim(); if (readPreference) { params.set('readPreference', readPreference); } const authMechanism = String(values.mongoAuthMechanism || '').trim(); if (authMechanism) { params.set('authMechanism', authMechanism); } if (Number.isFinite(timeout) && timeout > 0) { params.set('connectTimeoutMS', String(timeout * 1000)); params.set('serverSelectionTimeoutMS', String(timeout * 1000)); } const dbPath = database ? `/${encodeURIComponent(database)}` : '/'; const query = params.toString(); return `${scheme}://${encodedAuth}${hosts.join(',')}${dbPath}${query ? `?${query}` : ''}`; } const scheme = type === 'postgres' ? 'postgresql' : type; const dbPath = database ? `/${encodeURIComponent(database)}` : ''; return `${scheme}://${encodedAuth}${toAddress(host, port, defaultPort)}${dbPath}`; }; const handleGenerateURI = () => { try { const values = form.getFieldsValue(true); const uri = buildUriFromValues(values); form.setFieldValue('uri', uri); message.success('URI 已生成'); } catch { message.error('生成 URI 失败'); } }; const handleParseURI = () => { const uriText = String(form.getFieldValue('uri') || '').trim(); const type = String(form.getFieldValue('type') || dbType).trim().toLowerCase(); if (!uriText) { message.warning('请先输入 URI'); return; } const parsedValues = parseUriToValues(uriText, type); if (!parsedValues) { message.error('当前 URI 与数据源类型不匹配,或 URI 格式不支持'); return; } form.setFieldsValue({ ...parsedValues, uri: uriText }); if (testResult) { setTestResult(null); } message.success('已根据 URI 回填连接参数'); }; const handleCopyURI = async () => { let uriText = String(form.getFieldValue('uri') || '').trim(); if (!uriText) { const values = form.getFieldsValue(true); uriText = buildUriFromValues(values); form.setFieldValue('uri', uriText); } if (!uriText) { message.warning('没有可复制的 URI'); return; } try { await navigator.clipboard.writeText(uriText); message.success('URI 已复制'); } catch { message.error('复制失败'); } }; useEffect(() => { if (open) { setTestResult(null); // Reset test result setDbList([]); setRedisDbList([]); setMongoMembers([]); if (initialValues) { // Edit mode: Go directly to step 2 setStep(2); const config: any = initialValues.config || {}; const configType = String(config.type || 'mysql'); const defaultPort = getDefaultPortByType(configType); const normalizedHosts = normalizeAddressList(config.hosts, defaultPort); const primaryAddress = parseHostPort( normalizedHosts[0] || toAddress(config.host || 'localhost', Number(config.port || defaultPort), defaultPort), defaultPort ); const primaryHost = primaryAddress?.host || String(config.host || 'localhost'); const primaryPort = primaryAddress?.port || Number(config.port || defaultPort); const mysqlReplicaHosts = (configType === 'mysql' || configType === 'mariadb' || configType === 'sphinx') ? normalizedHosts.slice(1) : []; const mongoHosts = configType === 'mongodb' ? normalizedHosts.slice(1) : []; const mysqlIsReplica = String(config.topology || '').toLowerCase() === 'replica' || mysqlReplicaHosts.length > 0; const mongoIsReplica = String(config.topology || '').toLowerCase() === 'replica' || mongoHosts.length > 0 || !!config.replicaSet; form.setFieldsValue({ type: configType, name: initialValues.name, host: primaryHost, port: primaryPort, user: config.user, password: config.password, database: config.database, uri: config.uri || '', includeDatabases: initialValues.includeDatabases, includeRedisDatabases: initialValues.includeRedisDatabases, useSSH: config.useSSH, sshHost: config.ssh?.host, sshPort: config.ssh?.port, sshUser: config.ssh?.user, sshPassword: config.ssh?.password, sshKeyPath: config.ssh?.keyPath, driver: config.driver, dsn: config.dsn, timeout: config.timeout || 30, mysqlTopology: mysqlIsReplica ? 'replica' : 'single', mysqlReplicaHosts: mysqlReplicaHosts, mysqlReplicaUser: config.mysqlReplicaUser || '', mysqlReplicaPassword: config.mysqlReplicaPassword || '', mongoTopology: mongoIsReplica ? 'replica' : 'single', mongoHosts: mongoHosts, mongoSrv: !!config.mongoSrv, mongoReplicaSet: config.replicaSet || '', mongoAuthSource: config.authSource || '', mongoReadPreference: config.readPreference || 'primary', mongoAuthMechanism: config.mongoAuthMechanism || '', savePassword: config.savePassword !== false, mongoReplicaUser: config.mongoReplicaUser || '', mongoReplicaPassword: config.mongoReplicaPassword || '' }); setUseSSH(config.useSSH || false); setDbType(configType); // 如果是 Redis 编辑模式,设置已保存的 Redis 数据库列表 if (configType === '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, true); const displayHost = String((config as any).host || values.host || '').trim(); 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 ${displayHost}` : displayHost)), 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, false); // 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 handleDiscoverMongoMembers = async () => { if (discoveringMembers || dbType !== 'mongodb') { return; } try { const values = await form.validateFields(); setDiscoveringMembers(true); const config = await buildConfig(values, false); const result = await MongoDiscoverMembers(config as any); if (!result.success) { message.error(result.message || '成员发现失败'); return; } const data = (result.data as Record) || {}; const membersRaw = Array.isArray(data.members) ? data.members : []; const members: MongoMemberInfo[] = membersRaw .map((item: any) => ({ host: String(item.host || '').trim(), role: String(item.role || item.state || 'UNKNOWN').trim(), state: String(item.state || item.role || 'UNKNOWN').trim(), stateCode: Number(item.stateCode || 0), healthy: !!item.healthy, isSelf: !!item.isSelf, })) .filter((item: MongoMemberInfo) => !!item.host); setMongoMembers(members); if (!form.getFieldValue('mongoReplicaSet') && data.replicaSet) { form.setFieldValue('mongoReplicaSet', String(data.replicaSet)); } message.success(result.message || `发现 ${members.length} 个成员`); } catch (error: any) { message.error(error?.message || '成员发现失败'); } finally { setDiscoveringMembers(false); } }; const buildConfig = async (values: any, forPersist: boolean) => { const mergedValues = { ...values }; const parsedUriValues = parseUriToValues(mergedValues.uri, mergedValues.type); const isEmptyField = (value: unknown) => ( value === undefined || value === null || value === '' || value === 0 || (Array.isArray(value) && value.length === 0) ); if (parsedUriValues) { Object.entries(parsedUriValues).forEach(([key, value]) => { if (isEmptyField((mergedValues as any)[key])) { (mergedValues as any)[key] = value; } }); } const type = String(mergedValues.type || '').toLowerCase(); const defaultPort = getDefaultPortByType(type); const parsedPrimary = parseHostPort( toAddress(mergedValues.host || 'localhost', Number(mergedValues.port || defaultPort), defaultPort), defaultPort ); const primaryHost = parsedPrimary?.host || 'localhost'; const primaryPort = parsedPrimary?.port || defaultPort; let hosts: string[] = []; let topology: 'single' | 'replica' | undefined; let replicaSet = ''; let authSource = ''; let readPreference = ''; let mysqlReplicaUser = ''; let mysqlReplicaPassword = ''; let mongoSrvEnabled = false; let mongoAuthMechanism = ''; let mongoReplicaUser = ''; let mongoReplicaPassword = ''; const savePassword = type === 'mongodb' ? mergedValues.savePassword !== false : true; if (type === 'mysql' || type === 'mariadb' || type === 'sphinx') { const replicas = mergedValues.mysqlTopology === 'replica' ? normalizeAddressList(mergedValues.mysqlReplicaHosts, defaultPort) : []; const allHosts = normalizeAddressList([`${primaryHost}:${primaryPort}`, ...replicas], defaultPort); if (mergedValues.mysqlTopology === 'replica' || allHosts.length > 1) { hosts = allHosts; topology = 'replica'; mysqlReplicaUser = String(mergedValues.mysqlReplicaUser || '').trim(); mysqlReplicaPassword = String(mergedValues.mysqlReplicaPassword || ''); } else { topology = 'single'; } } if (type === 'mongodb') { mongoSrvEnabled = !!mergedValues.mongoSrv; const extraHosts = mergedValues.mongoTopology === 'replica' ? (mongoSrvEnabled ? normalizeMongoSrvHostList(mergedValues.mongoHosts, defaultPort) : normalizeAddressList(mergedValues.mongoHosts, defaultPort)) : []; const primarySeed = mongoSrvEnabled ? primaryHost : `${primaryHost}:${primaryPort}`; const allHosts = mongoSrvEnabled ? normalizeMongoSrvHostList([primarySeed, ...extraHosts], defaultPort) : normalizeAddressList([primarySeed, ...extraHosts], defaultPort); if (mergedValues.mongoTopology === 'replica' || allHosts.length > 1 || mergedValues.mongoReplicaSet) { hosts = allHosts; topology = 'replica'; mongoReplicaUser = String(mergedValues.mongoReplicaUser || '').trim(); mongoReplicaPassword = String(mergedValues.mongoReplicaPassword || ''); } else { topology = 'single'; } replicaSet = String(mergedValues.mongoReplicaSet || '').trim(); authSource = String(mergedValues.mongoAuthSource || mergedValues.database || 'admin').trim(); readPreference = String(mergedValues.mongoReadPreference || 'primary').trim(); mongoAuthMechanism = String(mergedValues.mongoAuthMechanism || '').trim().toUpperCase(); } const sshConfig = mergedValues.useSSH ? { host: mergedValues.sshHost, port: Number(mergedValues.sshPort), user: mergedValues.sshUser, password: mergedValues.sshPassword || "", keyPath: mergedValues.sshKeyPath || "" } : { host: "", port: 22, user: "", password: "", keyPath: "" }; const keepPassword = !forPersist || savePassword; return { type: mergedValues.type, host: primaryHost, port: Number(primaryPort || 0), user: mergedValues.user || "", password: keepPassword ? (mergedValues.password || "") : "", savePassword: savePassword, database: mergedValues.database || "", useSSH: !!mergedValues.useSSH, ssh: sshConfig, driver: mergedValues.driver, dsn: mergedValues.dsn, timeout: Number(mergedValues.timeout || 30), uri: String(mergedValues.uri || '').trim(), hosts: hosts, topology: topology, mysqlReplicaUser: mysqlReplicaUser, mysqlReplicaPassword: keepPassword ? mysqlReplicaPassword : "", replicaSet: replicaSet, authSource: authSource, readPreference: readPreference, mongoSrv: mongoSrvEnabled, mongoAuthMechanism: mongoAuthMechanism, mongoReplicaUser: mongoReplicaUser, mongoReplicaPassword: keepPassword ? mongoReplicaPassword : "", }; }; const handleTypeSelect = (type: string) => { setDbType(type); form.setFieldsValue({ type: type }); const defaultPort = getDefaultPortByType(type); if (type !== 'sqlite' && type !== 'custom') { form.setFieldsValue({ port: defaultPort, mysqlTopology: 'single', mongoTopology: 'single', mongoSrv: false, mongoReadPreference: 'primary', mongoReplicaSet: '', mongoAuthSource: '', mongoAuthMechanism: '', savePassword: true, mysqlReplicaHosts: [], mongoHosts: [], mysqlReplicaUser: '', mysqlReplicaPassword: '', mongoReplicaUser: '', mongoReplicaPassword: '', }); } setMongoMembers([]); 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: 'sphinx', name: 'Sphinx', 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: 'tdengine', name: 'TDengine', 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); if ( changed.type !== undefined || changed.host !== undefined || changed.port !== undefined || changed.mongoHosts !== undefined || changed.mongoTopology !== undefined || changed.mongoSrv !== undefined ) { setMongoMembers([]); } }} > {/* Hidden Type Field to keep form value synced */} {isCustom ? ( <> ) : ( <>
{!isSqlite && ( Number(value) > 0)]} style={{ width: 100 }} > )}
{(dbType === 'mysql' || dbType === 'mariadb' || dbType === 'sphinx') && ( <>
)} )} {dbType === 'mongodb' && ( <> 使用 SRV 记录(mongodb+srv) )} {mongoSrv && ( )}
发现后可校验当前副本集状态 {mongoMembers.length > 0 && ( `${record.host}-${record.state}`} dataSource={mongoMembers} style={{ marginBottom: 12 }} columns={[ { title: '成员', dataIndex: 'host', width: '48%', render: (value: string, record: MongoMemberInfo) => ( {value} {record.isSelf ? 当前 : null} ), }, { title: '状态', dataIndex: 'state', width: '32%', render: (value: string) => { const state = String(value || '').toUpperCase(); let color: string = 'default'; if (state === 'PRIMARY') color = 'success'; else if (state === 'SECONDARY' || state === 'PASSIVE') color = 'blue'; else if (state === 'ARBITER') color = 'purple'; else if (state === 'DOWN' || state === 'REMOVED' || state === 'UNKNOWN') color = 'error'; return {state || 'UNKNOWN'}; }, }, { title: '健康', dataIndex: 'healthy', width: '20%', render: (value: boolean) => ( {value ? '正常' : '异常'} ), }, ]} /> )} )} {redisDbList.map(db => db{db})} )} {/* Non-Redis, non-SQLite: username and password */} {!isSqlite && !isRedis && (
{dbType === 'mongodb' && ( {dbList.map(db => {db})} )} {!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} 连接`; }; const modalBodyStyle = step === 1 ? { padding: '16px 24px', overflow: 'hidden' as const } : { padding: '16px 24px', maxHeight: 'calc(100vh - 220px)', overflowY: 'auto' as const, overflowX: 'hidden' as const, }; return ( {step === 1 ? renderStep1() : renderStep2()} ); }; export default ConnectionModal;