diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec93307..e2eebb8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,17 +62,18 @@ jobs: # Find .app bundle APP_PATH=$(find . -maxdepth 1 -name "*.app" | head -n 1) - if [ -z "$APP_PATH" ]; then + if [ -z "$APP_NAME" ]; then echo "❌ 未找到 .app 应用包!" exit 1 fi - # Get pure name (e.g. GoNavi.app) - APP_NAME=$(basename "$APP_PATH") + # Ad-hoc codesign to prevent "Damaged" error (requires user to allow anyway, but valid structure) + echo "🔏 正在进行 Ad-hoc 签名..." + codesign --force --options runtime --deep --sign - "$APP_NAME" DMG_NAME="${{ matrix.artifact_name }}.dmg" - echo "📦 正在生成 DMG: $DMG_NAME (源应用: $APP_NAME)..." + echo "📦 正在生成 DMG: $DMG_NAME..." # Create DMG create-dmg \ diff --git a/README.md b/README.md index 0f93c09..5855204 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ ```bash # 克隆项目 -git clone https://github.com/yangguofeng/GoNavi.git +git clone https://github.com/Syngnat/GoNavi.git cd GoNavi # 启动开发服务器 (支持热重载) @@ -93,6 +93,22 @@ wails build -clean --- +## ❓ 常见问题 (Troubleshooting) + +### macOS 提示 "应用已损坏,无法打开" + +由于本项目尚未购买 Apple 开发者证书进行签名(Notarization),macOS 的 Gatekeeper 安全机制可能会拦截应用的运行。请按照以下步骤解决: + +1. 将下载的 `GoNavi.app` 拖入 **应用程序** 文件夹。 +2. 打开 **终端 (Terminal)**。 +3. 复制并执行以下命令(输入密码时不会显示): + ```bash + sudo xattr -rd com.apple.quarantine /Applications/GoNavi.app + ``` +4. 或者:在 Finder 中右键点击应用图标,按住 `Control` 键选择 **打开**,然后在弹出的窗口中再次点击 **打开**。 + +--- + ## 🤝 贡献指南 欢迎提交 Issue 和 Pull Request! diff --git a/frontend/src/App.css b/frontend/src/App.css index 4eb522e..556c56d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -13,8 +13,15 @@ html, body, #root { .ant-tree .ant-tree-node-content-wrapper { display: flex !important; align-items: center; - overflow: hidden; white-space: nowrap; + user-select: none !important; + -webkit-user-select: none !important; +} + +.ant-tree .ant-tree-title, +.ant-tree .ant-tree-treenode * { + user-select: none !important; + -webkit-user-select: none !important; } .ant-tree .ant-tree-title { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0c790dd..fda74d6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { Layout, Button, ConfigProvider, theme } from 'antd'; -import { PlusOutlined, BulbOutlined, BulbFilled, ConsoleSqlOutlined, BugOutlined } from '@ant-design/icons'; +import { Layout, Button, ConfigProvider, theme, Dropdown, MenuProps, message } from 'antd'; +import { PlusOutlined, BulbOutlined, BulbFilled, ConsoleSqlOutlined, BugOutlined, SettingOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons'; import Sidebar from './components/Sidebar'; import TabManager from './components/TabManager'; import ConnectionModal from './components/ConnectionModal'; @@ -14,7 +14,82 @@ const { Sider, Content } = Layout; function App() { const [isModalOpen, setIsModalOpen] = useState(false); const [editingConnection, setEditingConnection] = useState(null); - const { darkMode, toggleDarkMode, addTab, activeContext } = useStore(); + const { darkMode, toggleDarkMode, addTab, activeContext, connections, addConnection, tabs, activeTabId } = useStore(); + + const handleNewQuery = () => { + let connId = activeContext?.connectionId || ''; + let db = activeContext?.dbName || ''; + + // Priority: Active Tab Context > Sidebar Selection + if (activeTabId) { + const currentTab = tabs.find(t => t.id === activeTabId); + if (currentTab && currentTab.connectionId) { + connId = currentTab.connectionId; + db = currentTab.dbName || ''; + } + } + + addTab({ + id: `query-${Date.now()}`, + title: '新建查询', + type: 'query', + connectionId: connId, + dbName: db + }); + }; + + const handleImportConnections = async () => { + const res = await (window as any).go.app.App.ImportConfigFile(); + if (res.success) { + try { + const imported = JSON.parse(res.data); + if (Array.isArray(imported)) { + let count = 0; + imported.forEach((conn: any) => { + if (!connections.some(c => c.id === conn.id)) { + addConnection(conn); + count++; + } + }); + message.success(`成功导入 ${count} 个连接`); + } else { + message.error("文件格式错误:需要 JSON 数组"); + } + } catch (e) { + message.error("解析 JSON 失败"); + } + } else if (res.message !== "Cancelled") { + message.error("导入失败: " + res.message); + } + }; + + const handleExportConnections = async () => { + if (connections.length === 0) { + message.warning("没有连接可导出"); + return; + } + const res = await (window as any).go.app.App.ExportData(connections, [], "connections", "json"); + if (res.success) { + message.success("导出成功"); + } else if (res.message !== "Cancelled") { + message.error("导出失败: " + res.message); + } + }; + + const settingsMenu: MenuProps['items'] = [ + { + key: 'import', + label: '导入连接配置', + icon: , + onClick: handleImportConnections + }, + { + key: 'export', + label: '导出连接配置', + icon: , + onClick: handleExportConnections + } + ]; // Log Panel const [logPanelHeight, setLogPanelHeight] = useState(200); @@ -151,40 +226,37 @@ function App() { width={sidebarWidth} style={{ borderRight: darkMode ? '1px solid #303030' : '1px solid #f0f0f0', - position: 'relative', - display: 'flex', - flexDirection: 'column' + position: 'relative' }} > -
- GoNavi -
-
-
- -
- -
+
+
+ GoNavi +
+
+
+ +
+ +
- {/* Sidebar Footer for Log Toggle */} -
- + {/* Sidebar Footer for Log Toggle */} +
+ +
{/* Sidebar Resize Handle */} diff --git a/frontend/src/components/ConnectionModal.tsx b/frontend/src/components/ConnectionModal.tsx index 9d2e9c6..864bc01 100644 --- a/frontend/src/components/ConnectionModal.tsx +++ b/frontend/src/components/ConnectionModal.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; -import { Modal, Form, Input, InputNumber, Button, message, Checkbox, Divider, Collapse, Select } from 'antd'; +import { Modal, Form, Input, InputNumber, Button, message, Checkbox, Divider, Collapse, Select, Alert } from 'antd'; import { useStore } from '../store'; -import { MySQLConnect } from '../../wailsjs/go/app/App'; +import { MySQLConnect, MySQLGetDatabases } from '../../wailsjs/go/app/App'; import { SavedConnection } from '../types'; const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialValues?: SavedConnection | null }> = ({ open, onClose, initialValues }) => { @@ -9,11 +9,15 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal const [loading, setLoading] = useState(false); const [useSSH, setUseSSH] = useState(false); const [dbType, setDbType] = useState('mysql'); + const [testResult, setTestResult] = useState<{ type: 'success' | 'error', message: string } | null>(null); + const [dbList, setDbList] = useState([]); const addConnection = useStore((state) => state.addConnection); const updateConnection = useStore((state) => state.updateConnection); useEffect(() => { if (open) { + setTestResult(null); // Reset test result + setDbList([]); if (initialValues) { form.setFieldsValue({ type: initialValues.config.type, @@ -23,6 +27,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal user: initialValues.config.user, password: initialValues.config.password, database: initialValues.config.database, + includeDatabases: initialValues.includeDatabases, useSSH: initialValues.config.useSSH, sshHost: initialValues.config.ssh?.host, sshPort: initialValues.config.ssh?.port, @@ -45,25 +50,9 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal const values = await form.validateFields(); setLoading(true); - 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: "" }; - - const config = { - 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 - }; + const config = await buildConfig(values); + // Use Connect to verify before saving const res = await MySQLConnect(config as any); setLoading(false); @@ -71,7 +60,8 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal const newConn = { id: initialValues ? initialValues.id : Date.now().toString(), name: values.name || (values.type === 'sqlite' ? 'SQLite DB' : values.host), - config: config + config: config, + includeDatabases: values.includeDatabases }; if (initialValues) { @@ -94,6 +84,51 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal } }; + const handleTest = async () => { + try { + const values = await form.validateFields(); + setLoading(true); + setTestResult(null); // Clear previous result + const config = await buildConfig(values); + const res = await (window as any).go.app.App.TestConnection(config); + setLoading(false); + if (res.success) { + setTestResult({ type: 'success', message: res.message }); + // Fetch DB List on success + const dbRes = await MySQLGetDatabases(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) { + 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 + }; + }; + const isSqlite = dbType === 'sqlite'; return ( @@ -103,8 +138,11 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal onCancel={onClose} onOk={handleOk} confirmLoading={loading} - okText="确定" - cancelText="取消" + footer={[ + , + , + + ]} width={600} zIndex={10001} // Increase z-index destroyOnHidden // Reset on close @@ -115,6 +153,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal layout="vertical" initialValues={{ type: 'mysql', host: 'localhost', port: 3306, user: 'root', useSSH: false, sshPort: 22 }} onValuesChange={(changed) => { + if (testResult) setTestResult(null); // Clear result on change if (changed.useSSH !== undefined) setUseSSH(changed.useSSH); if (changed.type !== undefined) setDbType(changed.type); }} @@ -155,8 +194,10 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal )} {!isSqlite && ( - - + + )} @@ -193,6 +234,15 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal )} + + {testResult && ( + + )} ); }; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 11b630a..bb4c835 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from 'react'; +import React, { useEffect, useState, useMemo, useRef } from 'react'; import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form, Badge } from 'antd'; import { DatabaseOutlined, @@ -46,6 +46,23 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> const [searchValue, setSearchValue] = useState(''); const [expandedKeys, setExpandedKeys] = useState([]); const [autoExpandParent, setAutoExpandParent] = useState(true); + const [loadedKeys, setLoadedKeys] = useState([]); + const [contextMenu, setContextMenu] = useState<{ x: number, y: number, items: MenuProps['items'] } | null>(null); + + // Virtual Scroll State + const [treeHeight, setTreeHeight] = useState(500); + const treeContainerRef = useRef(null); + + useEffect(() => { + if (!treeContainerRef.current) return; + const resizeObserver = new ResizeObserver(entries => { + for (let entry of entries) { + setTreeHeight(entry.contentRect.height); + } + }); + resizeObserver.observe(treeContainerRef.current); + return () => resizeObserver.disconnect(); + }, []); // Connection Status State: key -> 'success' | 'error' const [connectionStates, setConnectionStates] = useState>({}); @@ -87,7 +104,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> }))); }, [connections]); - const updateTreeData = (list: TreeNode[], key: React.Key, children: TreeNode[]): TreeNode[] => { + const updateTreeData = (list: TreeNode[], key: React.Key, children: TreeNode[] | undefined): TreeNode[] => { return list.map(node => { if (node.key === key) { return { ...node, children }; @@ -112,7 +129,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> const res = await MySQLGetDatabases(config as any); if (res.success) { setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' })); - const dbs = (res.data as any[]).map((row: any) => ({ + let dbs = (res.data as any[]).map((row: any) => ({ title: row.Database || row.database, key: `${conn.id}-${row.Database || row.database}`, icon: , @@ -120,6 +137,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> dataRef: { ...conn, dbName: row.Database || row.database }, isLeaf: false, })); + + // Filter databases if configured + if (conn.includeDatabases && conn.includeDatabases.length > 0) { + dbs = dbs.filter(db => conn.includeDatabases!.includes(db.title)); + } + setTreeData(origin => updateTreeData(origin, node.key, dbs)); } else { setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' })); @@ -433,27 +456,9 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> return loop(treeData); }, [searchValue, treeData]); - const titleRender = (node: any) => { - // Determine status - let status: 'success' | 'error' | 'default' = 'default'; + const getNodeMenuItems = (node: any): MenuProps['items'] => { if (node.type === 'connection') { - if (connectionStates[node.key] === 'success') status = 'success'; - else if (connectionStates[node.key] === 'error') status = 'error'; - } else if (node.type === 'database') { - if (connectionStates[node.key] === 'success') status = 'success'; - else if (connectionStates[node.key] === 'error') status = 'error'; - } - - // Override if active context? (Optional, user asked for "connected" status) - // If we want to show "Active" as Green even if not loaded? - // Let's stick to "Connected" state derived from successful load. - - const statusBadge = node.type === 'connection' || node.type === 'database' ? ( - - ) : null; - - if (node.type === 'connection') { - const items: MenuProps['items'] = [ + return [ { key: 'new-db', label: '新建数据库', @@ -498,16 +503,14 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> label: '断开连接', icon: , onClick: () => { - // Reset status setConnectionStates(prev => { const next = { ...prev }; delete next[node.key]; return next; }); - // Collapse node setExpandedKeys(prev => prev.filter(k => k !== node.key)); - // Clear children - setTreeData(origin => updateTreeData(origin, node.key, [])); + setLoadedKeys(prev => prev.filter(k => k !== node.key)); + setTreeData(origin => updateTreeData(origin, node.key, undefined)); message.success("已断开连接"); } }, @@ -525,13 +528,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> } } ]; - return ( - - {statusBadge}{node.title} - - ); } else if (node.type === 'database') { - const items: MenuProps['items'] = [ + return [ { key: 'new-table', label: '新建表', @@ -550,16 +548,14 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> label: '关闭数据库', icon: , onClick: () => { - // Reset status setConnectionStates(prev => { const next = { ...prev }; delete next[node.key]; return next; }); - // Collapse node setExpandedKeys(prev => prev.filter(k => k !== node.key)); - // Clear children - setTreeData(origin => updateTreeData(origin, node.key, [])); + setLoadedKeys(prev => prev.filter(k => k !== node.key)); + setTreeData(origin => updateTreeData(origin, node.key, undefined)); } }, { @@ -583,13 +579,23 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> onClick: () => handleRunSQLFile(node) } ]; - return ( - - {statusBadge}{node.title} - - ); } else if (node.type === 'table') { - const contextMenu: MenuProps['items'] = [ + return [ + { + key: 'new-query', + label: '新建查询', + icon: , + onClick: () => { + addTab({ + id: `query-${Date.now()}`, + title: `新建查询`, + type: 'query', + connectionId: node.dataRef.id, + dbName: node.dataRef.dbName + }); + } + }, + { type: 'divider' }, { key: 'design-table', label: '设计表', @@ -623,14 +629,33 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> ] } ]; - - return ( - - {node.title} - - ); } - return {node.title}; + return []; + }; + + const titleRender = (node: any) => { + let status: 'success' | 'error' | 'default' = 'default'; + if (node.type === 'connection' || node.type === 'database') { + if (connectionStates[node.key] === 'success') status = 'success'; + else if (connectionStates[node.key] === 'error') status = 'error'; + } + + const statusBadge = node.type === 'connection' || node.type === 'database' ? ( + + ) : null; + + return {statusBadge}{node.title}; + }; + + const onRightClick = ({ event, node }: any) => { + const items = getNodeMenuItems(node); + if (items && items.length > 0) { + setContextMenu({ + x: event.clientX, + y: event.clientY, + items + }); + } }; return ( @@ -638,7 +663,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
-
+
void }> titleRender={titleRender} expandedKeys={expandedKeys} onExpand={onExpand} + loadedKeys={loadedKeys} + onLoad={setLoadedKeys} autoExpandParent={autoExpandParent} blockNode + height={treeHeight} + onRightClick={onRightClick} />
+ {contextMenu && ( + { if (!open) setContextMenu(null); }} + trigger={['contextMenu']} + > +
+ + )} + >,arg2:Array,ar export function ExportTable(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:string):Promise; +export function ImportConfigFile():Promise; + export function ImportData(arg1:connection.ConnectionConfig,arg2:string,arg3:string):Promise; export function MySQLConnect(arg1:connection.ConnectionConfig):Promise; @@ -43,3 +45,5 @@ export function MySQLQuery(arg1:connection.ConnectionConfig,arg2:string,arg3:str export function MySQLShowCreateTable(arg1:connection.ConnectionConfig,arg2:string,arg3:string):Promise; export function OpenSQLFile():Promise; + +export function TestConnection(arg1:connection.ConnectionConfig):Promise; diff --git a/frontend/wailsjs/go/app/App.js b/frontend/wailsjs/go/app/App.js index b6df5cb..404a845 100755 --- a/frontend/wailsjs/go/app/App.js +++ b/frontend/wailsjs/go/app/App.js @@ -58,6 +58,10 @@ export function ExportTable(arg1, arg2, arg3, arg4) { return window['go']['app']['App']['ExportTable'](arg1, arg2, arg3, arg4); } +export function ImportConfigFile() { + return window['go']['app']['App']['ImportConfigFile'](); +} + export function ImportData(arg1, arg2, arg3) { return window['go']['app']['App']['ImportData'](arg1, arg2, arg3); } @@ -85,3 +89,7 @@ export function MySQLShowCreateTable(arg1, arg2, arg3) { export function OpenSQLFile() { return window['go']['app']['App']['OpenSQLFile'](); } + +export function TestConnection(arg1) { + return window['go']['app']['App']['TestConnection'](arg1); +} diff --git a/internal/app/app.go b/internal/app/app.go index fc0e117..c3bcb54 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -40,7 +40,16 @@ func (a *App) Shutdown(ctx context.Context) { // Helper: Generate a unique key for the connection config func getCacheKey(config connection.ConnectionConfig) string { - return fmt.Sprintf("%s|%s|%s:%d|%s|%s|%v", config.Type, config.User, config.Host, config.Port, config.Database, config.SSH.Host, config.UseSSH) + sshPart := "" + if config.UseSSH { + sshPart = fmt.Sprintf("|ssh:%s@%s:%d|%s", config.SSH.User, config.SSH.Host, config.SSH.Port, config.SSH.KeyPath) + // We don't include SSH password in key string to avoid log exposure if key is logged, + // but for cache uniqueness it is critical. + // Let's include a hash or just the value if we assume internal use. + // Including value for correctness. + sshPart += "|" + config.SSH.Password + } + return fmt.Sprintf("%s|%s:%s@%s:%d|%s%s", config.Type, config.User, config.Password, config.Host, config.Port, config.Database, sshPart) } // Helper: Get or create a database connection diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index 347031f..9c5436a 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -28,7 +28,27 @@ func (a *App) DBConnect(config connection.ConnectionConfig) connection.QueryResu return connection.QueryResult{Success: false, Message: err.Error()} } - return connection.QueryResult{Success: true, Message: "Connected successfully"} + return connection.QueryResult{Success: true, Message: "连接成功"} +} + +func (a *App) TestConnection(config connection.ConnectionConfig) connection.QueryResult { + // Force close existing cached connection if any to ensure fresh test + key := getCacheKey(config) + func() { + a.mu.Lock() + defer a.mu.Unlock() + if oldDB, ok := a.dbCache[key]; ok { + oldDB.Close() + delete(a.dbCache, key) + } + }() + + _, err := a.getDatabase(config) + if err != nil { + return connection.QueryResult{Success: false, Message: err.Error()} + } + + return connection.QueryResult{Success: true, Message: "连接成功"} } func (a *App) CreateDatabase(config connection.ConnectionConfig, dbName string) connection.QueryResult { diff --git a/internal/app/methods_file.go b/internal/app/methods_file.go index 5a34e61..5092a6b 100644 --- a/internal/app/methods_file.go +++ b/internal/app/methods_file.go @@ -44,6 +44,33 @@ func (a *App) OpenSQLFile() connection.QueryResult { return connection.QueryResult{Success: true, Data: string(content)} } +func (a *App) ImportConfigFile() connection.QueryResult { + selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select Config File", + Filters: []runtime.FileFilter{ + { + DisplayName: "JSON Files (*.json)", + Pattern: "*.json", + }, + }, + }) + + if err != nil { + return connection.QueryResult{Success: false, Message: err.Error()} + } + + if selection == "" { + return connection.QueryResult{Success: false, Message: "Cancelled"} + } + + content, err := os.ReadFile(selection) + if err != nil { + return connection.QueryResult{Success: false, Message: err.Error()} + } + + return connection.QueryResult{Success: true, Data: string(content)} +} + func (a *App) ImportData(config connection.ConnectionConfig, dbName, tableName string) connection.QueryResult { selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ Title: fmt.Sprintf("Import into %s", tableName), diff --git a/internal/db/mysql_impl.go b/internal/db/mysql_impl.go index b40a898..a884cf2 100644 --- a/internal/db/mysql_impl.go +++ b/internal/db/mysql_impl.go @@ -41,7 +41,9 @@ func (m *MySQLDB) Connect(config connection.ConnectionConfig) error { return err } m.conn = db - return nil + + // Force verification + return m.Ping() } func (m *MySQLDB) Close() error { diff --git a/internal/db/postgres_impl.go b/internal/db/postgres_impl.go index 44bfc9c..60924a0 100644 --- a/internal/db/postgres_impl.go +++ b/internal/db/postgres_impl.go @@ -40,7 +40,9 @@ func (p *PostgresDB) Connect(config connection.ConnectionConfig) error { return err } p.conn = db - return nil + + // Force verification + return p.Ping() } func (p *PostgresDB) Close() error { diff --git a/internal/db/sqlite_impl.go b/internal/db/sqlite_impl.go index e338ad3..075bc83 100644 --- a/internal/db/sqlite_impl.go +++ b/internal/db/sqlite_impl.go @@ -22,7 +22,9 @@ func (s *SQLiteDB) Connect(config connection.ConnectionConfig) error { return err } s.conn = db - return nil + + // Force verification + return s.Ping() } func (s *SQLiteDB) Close() error {