diff --git a/docs/HighGo_SM3_Integration_Guide.md b/docs/HighGo_SM3_Integration_Guide.md index c25034e..78d0842 100644 --- a/docs/HighGo_SM3_Integration_Guide.md +++ b/docs/HighGo_SM3_Integration_Guide.md @@ -17,17 +17,17 @@ HighGo(瀚高)数据库需要使用支持 SM3 国密认证的 PostgreSQL 驱 ### 步骤 2:放置驱动源码 -1. 在项目根目录创建 vendor 目录(如果不存在): +1. 在项目根目录创建目录(如果不存在): ```bash - mkdir -p vendor/highgo-pq + mkdir -p third_party/highgo-pq ``` -2. 解压下载的驱动源码到 `vendor/highgo-pq/` 目录 +2. 解压下载的驱动源码到 `third_party/highgo-pq/` 目录 3. 确保目录结构如下: ``` GoNavi/ - ├── vendor/ + ├── third_party/ │ └── highgo-pq/ │ ├── go.mod │ ├── conn.go @@ -36,10 +36,11 @@ HighGo(瀚高)数据库需要使用支持 SM3 国密认证的 PostgreSQL 驱 ### 步骤 3:修改 go.mod -在 `go.mod` 文件末尾添加 replace 指令: +在 `go.mod` 中添加独立的 HighGo 驱动依赖与本地替换: ```go -replace github.com/lib/pq => ./vendor/highgo-pq +require github.com/highgo/pq-sm3 v0.0.0 +replace github.com/highgo/pq-sm3 => ./third_party/highgo-pq ``` 完整示例: @@ -51,11 +52,24 @@ go 1.24.3 require ( // ... 现有依赖 github.com/lib/pq v1.11.1 + github.com/highgo/pq-sm3 v0.0.0 // ... 其他依赖 ) // 在文件末尾添加 -replace github.com/lib/pq => ./vendor/highgo-pq +replace github.com/highgo/pq-sm3 => ./third_party/highgo-pq +``` + +并将 `third_party/highgo-pq/go.mod` 的 module 修改为: + +```go +module github.com/highgo/pq-sm3 +``` + +同时在驱动源码中把注册名改为 `highgo`,确保不覆盖 `postgres`: + +```go +sql.Register("highgo", &Driver{}) ``` ### 步骤 4:更新 HighGo 连接配置(可选) @@ -100,10 +114,11 @@ q.Set("sslmode", "require") ### ⚠️ 影响范围 -使用 `go.mod replace` 会**全局替换** `github.com/lib/pq` 驱动,这意味着: +采用独立驱动名后,影响范围如下: -1. **PostgreSQL 连接也会使用瀚高驱动** -2. **需要验证瀚高驱动对标准 PostgreSQL 的兼容性** +1. **PostgreSQL 继续使用原生 `github.com/lib/pq`** +2. **HighGo 使用 `github.com/highgo/pq-sm3`(本地替换到官方源码)** +3. 两条连接链路互不覆盖,降低兼容性风险 ### 兼容性验证 @@ -112,23 +127,24 @@ q.Set("sslmode", "require") 1. ✅ HighGo 数据库连接(SM3 认证) 2. ✅ 标准 PostgreSQL 连接(确保仍然可用) -如果标准 PostgreSQL 连接失败,说明瀚高驱动不完全兼容,需要考虑其他方案。 +若 PostgreSQL 或 HighGo 任一连接异常,优先检查驱动注册名与 `go.mod` replace 是否一致。 ### 回滚方案 如果集成后出现问题,可以快速回滚: 1. 删除 `go.mod` 中的 replace 指令 -2. 删除 `vendor/highgo-pq/` 目录 -3. 运行 `go mod tidy` -4. 重新编译 +2. 删除 `go.mod` 中 `github.com/highgo/pq-sm3` 的 require +3. 删除 `third_party/highgo-pq/` 目录 +4. 运行 `go mod tidy` +5. 重新编译 ## 四、瀚高驱动特性 根据官方文档: -- **包路径**:`github.com/lib/pq`(与标准版相同) -- **驱动名**:`postgres`(与标准版相同) +- **项目内包路径**:`github.com/highgo/pq-sm3`(映射到本地 `third_party/highgo-pq`) +- **驱动名**:`highgo`(项目内独立注册,避免覆盖 `postgres`) - **SM3 支持**:自动启用国密认证 - **默认端口**:5866 - **默认数据库**:`highgo` @@ -139,11 +155,11 @@ q.Set("sslmode", "require") ### 问题 1:编译失败 -**现象**:`go build` 报错找不到 `github.com/lib/pq` +**现象**:`go build` 报错找不到 `github.com/highgo/pq-sm3` **解决**: -1. 检查 `vendor/highgo-pq/` 目录是否存在 -2. 检查 `go.mod` 中 replace 路径是否正确 +1. 检查 `third_party/highgo-pq/` 目录是否存在 +2. 检查 `go.mod` 中 `github.com/highgo/pq-sm3` 的 require/replace 是否正确 3. 运行 `go mod download` ### 问题 2:HighGo 连接失败 @@ -152,25 +168,26 @@ q.Set("sslmode", "require") **解决**: 1. 确认瀚高驱动已正确替换(检查 `go.mod`) -2. 确认 HighGo 服务器支持 SM3 认证 -3. 检查用户名、密码、端口是否正确 +2. 确认项目内驱动注册名为 `highgo` +3. 确认 HighGo 服务器支持 SM3 认证 +4. 检查用户名、密码、端口是否正确 ### 问题 3:PostgreSQL 连接失败 **现象**:集成后标准 PostgreSQL 无法连接 **解决**: -1. 这说明瀚高驱动不完全兼容标准 PostgreSQL -2. 需要考虑条件编译或其他隔离方案 -3. 临时回滚:删除 replace 指令 +1. 检查是否误将 `github.com/lib/pq` 全局 replace 到 HighGo 驱动 +2. 确认 PostgreSQL 仍使用 `sql.Open("postgres", dsn)` +3. 确认 HighGo 使用 `sql.Open("highgo", dsn)` ## 六、后续优化建议 -如果发现瀚高驱动与标准 PostgreSQL 不兼容,可以考虑: +如果后续需要增强,可考虑: -1. **条件编译**:使用 Go build tags 分别编译两个版本 -2. **动态驱动注册**:如果瀚高驱动支持自定义驱动名 -3. **联系瀚高技术支持**:咨询官方兼容性方案 +1. 将 HighGo `sslmode` 做成可配置项(前后端联动) +2. 增加 HighGo/PG 驱动链路健康检查项 +3. 联系瀚高技术支持确认 SM3 + SSL 最佳参数组合 ## 七、参考资料 diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 0f8f4fe..a7661c0 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -5b8157374dae5f9340e31b2d0bd2c00e \ No newline at end of file +d0f9366af59a6367ad3c7e2d4185ead4 \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css index 913fa7b..e532c84 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -72,6 +72,21 @@ body[data-theme='dark'] { overflow: hidden !important; } +.connection-modal-wrap .ant-modal-content { + max-height: calc(100vh - 72px); + display: flex; + flex-direction: column; +} + +.connection-modal-wrap .ant-modal-body { + flex: 1 1 auto; + min-height: 0; +} + +.connection-modal-wrap .ant-modal-footer { + flex-shrink: 0; +} + /* Custom Title Bar Close Button Hover */ .titlebar-close-btn:hover { background-color: #ff4d4f !important; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index de91ad8..2563846 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Layout, Button, ConfigProvider, theme, Dropdown, MenuProps, message, Modal, Spin, Slider, Progress } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import { PlusOutlined, BulbOutlined, BulbFilled, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined } from '@ant-design/icons'; -import { EventsOn } from '../wailsjs/runtime/runtime'; +import { Environment, EventsOn } from '../wailsjs/runtime/runtime'; import Sidebar from './components/Sidebar'; import TabManager from './components/TabManager'; import ConnectionModal from './components/ConnectionModal'; @@ -29,6 +29,7 @@ function App() { const effectiveBlur = normalizeBlurForPlatform(appearance.blur); const blurFilter = blurToFilter(effectiveBlur); const windowCornerRadius = 14; + const [isLinuxRuntime, setIsLinuxRuntime] = useState(false); // 同步 macOS 窗口透明度:opacity=1.0 且 blur=0 时关闭 NSVisualEffectView, // 避免 GPU 持续计算窗口背后的模糊合成 @@ -36,6 +37,23 @@ function App() { SetWindowTranslucency(appearance.opacity, appearance.blur).catch(() => {}); }, [appearance.opacity, appearance.blur]); + useEffect(() => { + let cancelled = false; + Environment() + .then((env) => { + if (cancelled) return; + setIsLinuxRuntime((env?.platform || '').toLowerCase() === 'linux'); + }) + .catch(() => { + if (cancelled) return; + const platform = typeof navigator !== 'undefined' ? navigator.platform : ''; + setIsLinuxRuntime(/linux/i.test(platform)); + }); + return () => { + cancelled = true; + }; + }, []); + // Background Helper const getBg = (darkHex: string, lightHex: string) => { if (!darkMode) return `rgba(255, 255, 255, ${effectiveOpacity})`; // Light mode usually white @@ -368,8 +386,13 @@ function App() { const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false); - // Log Panel - const [logPanelHeight, setLogPanelHeight] = useState(200); + // Log Panel: 最小高度按“工具栏 + 1 条日志行(微增)”限制 + const LOG_PANEL_TOOLBAR_HEIGHT = 32; + const LOG_PANEL_SINGLE_ROW_HEIGHT = 39; + const LOG_PANEL_MIN_VISIBLE_ROWS = 1; + const LOG_PANEL_MIN_HEIGHT = LOG_PANEL_TOOLBAR_HEIGHT + (LOG_PANEL_SINGLE_ROW_HEIGHT * LOG_PANEL_MIN_VISIBLE_ROWS); + const LOG_PANEL_MAX_HEIGHT = 800; + const [logPanelHeight, setLogPanelHeight] = useState(Math.max(200, LOG_PANEL_MIN_HEIGHT)); const [isLogPanelOpen, setIsLogPanelOpen] = useState(false); const logResizeRef = React.useRef<{ startY: number, startHeight: number } | null>(null); const logGhostRef = React.useRef(null); @@ -398,7 +421,10 @@ function App() { const handleLogResizeUp = (e: MouseEvent) => { if (logResizeRef.current) { const delta = logResizeRef.current.startY - e.clientY; - const newHeight = Math.max(100, Math.min(800, logResizeRef.current.startHeight + delta)); + const newHeight = Math.max( + LOG_PANEL_MIN_HEIGHT, + Math.min(LOG_PANEL_MAX_HEIGHT, logResizeRef.current.startHeight + delta) + ); setLogPanelHeight(newHeight); } @@ -550,6 +576,17 @@ function App() { }; }, []); + const linuxResizeHandleStyleBase = { + position: 'fixed', + zIndex: 12000, + background: 'transparent', + WebkitAppRegion: 'drag', + '--wails-draggable': 'drag', + userSelect: 'none' + } as any; + + const showLinuxResizeHandles = isLinuxRuntime; + return ( @@ -891,6 +928,21 @@ function App() { ) : null} + + {showLinuxResizeHandles && ( + <> + {/* Linux Mint 下 frameless 仅局部可缩放:补四边四角命中层 */} +
+
+
+
+ +
+
+
+
+ + )} {/* Ghost Resize Line for Sidebar */}
{ switch (type) { case 'mysql': return 3306; + case 'sphinx': return 9306; case 'postgres': return 5432; case 'redis': return 6379; case 'tdengine': return 6041; @@ -34,6 +38,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal 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 [testErrorLogOpen, setTestErrorLogOpen] = useState(false); const [dbList, setDbList] = useState([]); const [redisDbList, setRedisDbList] = useState([]); // Redis databases 0-15 const [mongoMembers, setMongoMembers] = useState([]); @@ -59,7 +64,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal const parsedPort = Number(portText); return { host: host || 'localhost', - port: Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : defaultPort, + port: Number.isFinite(parsedPort) && parsedPort > 0 && parsedPort <= 65535 ? parsedPort : defaultPort, }; } } @@ -72,7 +77,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal const parsedPort = Number(portText); return { host: host || 'localhost', - port: Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : defaultPort, + port: Number.isFinite(parsedPort) && parsedPort > 0 && parsedPort <= 65535 ? parsedPort : defaultPort, }; } @@ -104,6 +109,15 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal return result; }; + const isValidUriHostEntry = (entry: string): boolean => { + const text = String(entry || '').trim(); + if (!text) return false; + if (text.length > 255) return false; + // 拒绝明显的 DSN 片段或路径/空白,避免把非 URI 主机段误判为合法地址。 + if (/[()\\/\s]/.test(text)) return false; + return true; + }; + const normalizeMongoSrvHostList = (rawList: unknown, defaultPort: number): string[] => { const list = Array.isArray(rawList) ? rawList : []; const seen = new Set(); @@ -137,6 +151,10 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal return null; } let rest = uriText.slice(prefix.length); + const hashIndex = rest.indexOf('#'); + if (hashIndex >= 0) { + rest = rest.slice(0, hashIndex); + } let queryText = ''; const queryIndex = rest.indexOf('?'); if (queryIndex >= 0) { @@ -186,24 +204,40 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal if (!trimmedUri) { return null; } + if (trimmedUri.length > MAX_URI_LENGTH) { + return null; + } - if (type === 'mysql' || type === 'mariadb') { + 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, 3306); - const primary = parseHostPort(hostList[0] || 'localhost:3306', 3306); + if (!parsed.hosts.length || parsed.hosts.length > MAX_URI_HOSTS) { + return null; + } + if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) { + return null; + } + const hostList = normalizeAddressList(parsed.hosts, mysqlDefaultPort); + if (!hostList.length) { + return null; + } + const primary = parseHostPort(hostList[0] || `localhost:${mysqlDefaultPort}`, mysqlDefaultPort); const timeoutValue = Number(parsed.params.get('timeout')); + const topology = String(parsed.params.get('topology') || '').toLowerCase(); return { host: primary?.host || 'localhost', - port: primary?.port || 3306, + port: primary?.port || mysqlDefaultPort, user: parsed.username, password: parsed.password, database: parsed.database || '', - mysqlTopology: hostList.length > 1 || parsed.params.get('topology') === 'replica' ? 'replica' : 'single', + mysqlTopology: hostList.length > 1 || topology === 'replica' ? 'replica' : 'single', mysqlReplicaHosts: hostList.slice(1), - timeout: Number.isFinite(timeoutValue) && timeoutValue > 0 ? timeoutValue : undefined, + timeout: Number.isFinite(timeoutValue) && timeoutValue > 0 + ? Math.min(3600, Math.trunc(timeoutValue)) + : undefined, }; } @@ -212,10 +246,19 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal if (!parsed) { return null; } + if (!parsed.hosts.length || parsed.hosts.length > MAX_URI_HOSTS) { + return null; + } + if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) { + return null; + } const isSrv = trimmedUri.toLowerCase().startsWith('mongodb+srv://'); const hostList = isSrv ? normalizeMongoSrvHostList(parsed.hosts, 27017) : normalizeAddressList(parsed.hosts, 27017); + if (!hostList.length) { + return null; + } const primary = isSrv ? { host: hostList[0] || 'localhost', port: 27017 } : parseHostPort(hostList[0] || 'localhost:27017', 27017); @@ -233,7 +276,9 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal 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, + timeout: Number.isFinite(timeoutMs) && timeoutMs > 0 + ? Math.min(MAX_TIMEOUT_SECONDS, Math.ceil(timeoutMs / 1000)) + : undefined, savePassword: true, }; } @@ -259,8 +304,9 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal }); const getUriPlaceholder = () => { - if (dbType === 'mysql' || dbType === 'mariadb') { - return 'mysql://user:pass@127.0.0.1:3306,127.0.0.2:3306/db_name?topology=replica'; + 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'; @@ -281,12 +327,12 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal ? `${encodeURIComponent(user)}${password ? `:${encodeURIComponent(password)}` : ''}@` : ''; - if (type === 'mysql' || type === 'mariadb') { - const primary = toAddress(host, port, 3306); + if (type === 'mysql' || type === 'mariadb' || type === 'sphinx') { + const primary = toAddress(host, port, defaultPort); const replicas = values.mysqlTopology === 'replica' - ? normalizeAddressList(values.mysqlReplicaHosts, 3306) + ? normalizeAddressList(values.mysqlReplicaHosts, defaultPort) : []; - const hosts = normalizeAddressList([primary, ...replicas], 3306); + const hosts = normalizeAddressList([primary, ...replicas], defaultPort); const params = new URLSearchParams(); if (hosts.length > 1 || values.mysqlTopology === 'replica') { params.set('topology', 'replica'); @@ -354,22 +400,26 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal }; const handleParseURI = () => { - const uriText = String(form.getFieldValue('uri') || '').trim(); - const type = String(form.getFieldValue('type') || dbType).trim().toLowerCase(); - if (!uriText) { - message.warning('请先输入 URI'); - return; + try { + 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 回填连接参数'); + } catch { + message.error('URI 解析失败,请检查格式后重试'); } - 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 () => { @@ -394,6 +444,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal useEffect(() => { if (open) { setTestResult(null); // Reset test result + setTestErrorLogOpen(false); setDbList([]); setRedisDbList([]); setMongoMembers([]); @@ -410,7 +461,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal ); const primaryHost = primaryAddress?.host || String(config.host || 'localhost'); const primaryPort = primaryAddress?.port || Number(config.port || defaultPort); - const mysqlReplicaHosts = (configType === 'mysql' || configType === 'mariadb') ? normalizedHosts.slice(1) : []; + 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; @@ -520,6 +571,12 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal }, 0); }; + const buildTestFailureMessage = (reason: unknown, fallback: string) => { + const text = String(reason ?? '').trim(); + const normalized = text && text !== 'undefined' && text !== 'null' ? text : fallback; + return `测试失败: ${normalized}`; + }; + const handleTest = async () => { if (testInFlightRef.current) return; testInFlightRef.current = true; @@ -549,10 +606,23 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal } } } else { - setTestResult({ type: 'error', message: "测试失败: " + res.message }); + const failMessage = buildTestFailureMessage( + res?.message, + '连接被拒绝或参数无效,请检查后重试' + ); + setTestResult({ type: 'error', message: failMessage }); } - } catch (e) { - // ignore + } catch (e: unknown) { + if (e && typeof e === 'object' && 'errorFields' in e) { + const failMessage = '测试失败: 请先完善必填项后再测试连接'; + setTestResult({ type: 'error', message: failMessage }); + return; + } + const reason = e instanceof Error + ? e.message + : (typeof e === 'string' ? e : '未知异常'); + const failMessage = buildTestFailureMessage(reason, '未知异常'); + setTestResult({ type: 'error', message: failMessage }); } finally { testInFlightRef.current = false; setLoading(false); @@ -638,7 +708,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal ? mergedValues.savePassword !== false : true; - if (type === 'mysql' || type === 'mariadb') { + if (type === 'mysql' || type === 'mariadb' || type === 'sphinx') { const replicas = mergedValues.mysqlTopology === 'replica' ? normalizeAddressList(mergedValues.mysqlReplicaHosts, defaultPort) : []; @@ -753,6 +823,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal { 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: }, @@ -850,7 +921,10 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal mongoReplicaPassword: '', }} onValuesChange={(changed) => { - if (testResult) setTestResult(null); // Clear result on change + if (testResult) { + setTestResult(null); // Clear result on change + setTestErrorLogOpen(false); + } 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); @@ -920,7 +994,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal )}
- {(dbType === 'mysql' || dbType === 'mariadb') && ( + {(dbType === 'mysql' || dbType === 'mariadb' || dbType === 'sphinx') && ( <>