import React, { useEffect, useState, useMemo } from 'react'; import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form } from 'antd'; import { DatabaseOutlined, TableOutlined, ConsoleSqlOutlined, HddOutlined, FolderOpenOutlined, FileTextOutlined, CopyOutlined, ExportOutlined, SaveOutlined, EditOutlined, DownOutlined, SearchOutlined, KeyOutlined, ThunderboltOutlined, UnorderedListOutlined, FunctionOutlined, LinkOutlined, FileAddOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { SavedConnection } from '../types'; import { MySQLGetDatabases, MySQLGetTables, MySQLShowCreateTable, ExportTable, OpenSQLFile, CreateDatabase } from '../../wailsjs/go/app/App'; const { Search } = Input; interface TreeNode { title: string; key: string; isLeaf?: boolean; children?: TreeNode[]; icon?: React.ReactNode; dataRef?: any; type?: 'connection' | 'database' | 'table' | 'queries-folder' | 'saved-query' | 'folder-columns' | 'folder-indexes' | 'folder-fks' | 'folder-triggers'; } const Sidebar: React.FC = () => { const { connections, savedQueries, addTab, setActiveContext } = useStore(); const [treeData, setTreeData] = useState([]); const [searchValue, setSearchValue] = useState(''); const [expandedKeys, setExpandedKeys] = useState([]); const [autoExpandParent, setAutoExpandParent] = useState(true); // Create Database Modal const [isCreateDbModalOpen, setIsCreateDbModalOpen] = useState(false); const [createDbForm] = Form.useForm(); const [targetConnection, setTargetConnection] = useState(null); useEffect(() => { // Refresh queries for expanded databases const findNode = (nodes: TreeNode[], k: React.Key): TreeNode | null => { for (const node of nodes) { if (node.key === k) return node; if (node.children) { const res = findNode(node.children, k); if (res) return res; } } return null; }; expandedKeys.forEach(key => { const node = findNode(treeData, key); if (node && node.type === 'database') { loadTables(node); } }); }, [savedQueries]); useEffect(() => { setTreeData(connections.map(conn => ({ title: conn.name, key: conn.id, icon: , type: 'connection', dataRef: conn, isLeaf: false, }))); }, [connections]); const updateTreeData = (list: TreeNode[], key: React.Key, children: TreeNode[]): TreeNode[] => { return list.map(node => { if (node.key === key) { return { ...node, children }; } if (node.children) { return { ...node, children: updateTreeData(node.children, key, children) }; } return node; }); }; const loadDatabases = async (node: any) => { const conn = node.dataRef as SavedConnection; const config = { ...conn.config, port: Number(conn.config.port), password: conn.config.password || "", database: conn.config.database || "", useSSH: conn.config.useSSH || false, ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" } }; const res = await MySQLGetDatabases(config as any); if (res.success) { const dbs = (res.data as any[]).map((row: any) => ({ title: row.Database || row.database, key: `${conn.id}-${row.Database || row.database}`, icon: , type: 'database' as const, dataRef: { ...conn, dbName: row.Database || row.database }, isLeaf: false, })); setTreeData(origin => updateTreeData(origin, node.key, dbs)); } else { message.error(res.message); } }; const loadTables = async (node: any) => { const conn = node.dataRef; // has dbName const dbName = conn.dbName; const key = node.key; const dbQueries = savedQueries.filter(q => q.connectionId === conn.id && q.dbName === dbName); const queriesNode: TreeNode = { title: '已存查询', key: `${key}-queries`, icon: , type: 'queries-folder', isLeaf: dbQueries.length === 0, children: dbQueries.map(q => ({ title: q.name, key: q.id, icon: , type: 'saved-query', dataRef: q, isLeaf: true })) }; const config = { ...conn.config, port: Number(conn.config.port), password: conn.config.password || "", database: conn.config.database || "", useSSH: conn.config.useSSH || false, ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" } }; const res = await MySQLGetTables(config as any, conn.dbName); if (res.success) { const tables = (res.data as any[]).map((row: any) => { const tableName = Object.values(row)[0] as string; return { title: tableName, key: `${conn.id}-${conn.dbName}-${tableName}`, icon: , type: 'table' as const, dataRef: { ...conn, tableName }, isLeaf: false, }; }); setTreeData(origin => updateTreeData(origin, key, [queriesNode, ...tables])); } else { message.error(res.message); } }; const onLoadData = async ({ key, children, dataRef, type }: any) => { if (children) return; if (type === 'connection') { await loadDatabases({ key, dataRef }); } else if (type === 'database') { await loadTables({ key, dataRef }); } else if (type === 'table') { // Expand table to show object categories const { tableName, dbName, id } = dataRef; const conn = dataRef; const folders: TreeNode[] = [ { title: '列', key: `${key}-columns`, icon: , type: 'folder-columns', isLeaf: true, dataRef: conn }, { title: '索引', key: `${key}-indexes`, icon: , type: 'folder-indexes', isLeaf: true, dataRef: conn }, { title: '外键', key: `${key}-fks`, icon: , type: 'folder-fks', isLeaf: true, dataRef: conn }, { title: '触发器', key: `${key}-triggers`, icon: , type: 'folder-triggers', isLeaf: true, dataRef: conn } ]; setTreeData(origin => updateTreeData(origin, key, folders)); } }; const openDesign = (node: any, initialTab: string, readOnly: boolean = false) => { const { tableName, dbName, id } = node.dataRef; addTab({ id: `design-${id}-${dbName}-${tableName}`, title: `${readOnly ? '表结构' : '设计表'} (${tableName})`, type: 'design', connectionId: id, dbName: dbName, tableName: tableName, initialTab: initialTab, readOnly: readOnly }); }; const openNewTableDesign = (node: any) => { const { dbName, id } = node.dataRef; addTab({ id: `new-table-${id}-${dbName}-${Date.now()}`, title: `新建表 - ${dbName}`, type: 'design', connectionId: id, dbName: dbName, tableName: '', // Empty tableName signals creation mode initialTab: 'columns', readOnly: false }); }; const onSelect = (keys: React.Key[], info: any) => { if (!info.node.selected) { setActiveContext(null); return; } const { type, dataRef, key, title } = info.node; // Update active context if (type === 'connection') { setActiveContext({ connectionId: key, dbName: '' }); } else if (type === 'database') { setActiveContext({ connectionId: dataRef.id, dbName: title }); } else if (type === 'table') { setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName }); } else if (type === 'saved-query') { setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName }); } if (type === 'folder-columns') openDesign(info.node, 'columns', true); else if (type === 'folder-indexes') openDesign(info.node, 'indexes', true); else if (type === 'folder-fks') openDesign(info.node, 'foreignKeys', true); else if (type === 'folder-triggers') openDesign(info.node, 'triggers', true); }; const onExpand = (newExpandedKeys: React.Key[]) => { setExpandedKeys(newExpandedKeys); setAutoExpandParent(false); }; const onDoubleClick = (e: any, node: any) => { const key = node.key; const isExpanded = expandedKeys.includes(key); const newExpandedKeys = isExpanded ? expandedKeys.filter(k => k !== key) : [...expandedKeys, key]; setExpandedKeys(newExpandedKeys); if (!isExpanded) setAutoExpandParent(false); if (node.type === 'table') { const { tableName, dbName, id } = node.dataRef; addTab({ id: node.key, title: tableName, type: 'table', connectionId: id, dbName, tableName, }); } else if (node.type === 'saved-query') { const q = node.dataRef; addTab({ id: q.id, title: q.name, type: 'query', connectionId: q.connectionId, dbName: q.dbName, query: q.sql }); } }; const handleCopyStructure = async (node: any) => { const { config, dbName, tableName } = node.dataRef; const res = await MySQLShowCreateTable({ ...config, port: Number(config.port), password: config.password || "", database: config.database || "", useSSH: config.useSSH || false, ssh: config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" } } as any, dbName, tableName); if (res.success) { navigator.clipboard.writeText(res.data as string); message.success('表结构已复制到剪贴板'); } else { message.error(res.message); } }; const handleExport = async (node: any, format: string) => { const { config, dbName, tableName } = node.dataRef; const hide = message.loading(`正在导出 ${tableName} 为 ${format.toUpperCase()}...`, 0); const res = await ExportTable({ ...config, port: Number(config.port), password: config.password || "", database: config.database || "", useSSH: config.useSSH || false, ssh: config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" } } as any, dbName, tableName, format); hide(); if (res.success) { message.success('导出成功'); } else if (res.message !== 'Cancelled') { message.error('导出失败: ' + res.message); } }; const handleRunSQLFile = async (node: any) => { const res = await (window as any).go.app.App.OpenSQLFile(); if (res.success) { const sqlContent = res.data; const { dbName, id } = node.dataRef; addTab({ id: `query-${Date.now()}`, title: `Import SQL`, type: 'query', connectionId: node.type === 'connection' ? node.key : node.dataRef.id, dbName: dbName, query: sqlContent }); } else if (res.message !== "Cancelled") { message.error("读取文件失败: " + res.message); } }; const handleCreateDatabase = async () => { try { const values = await createDbForm.validateFields(); const conn = targetConnection.dataRef; const config = { ...conn.config, port: Number(conn.config.port), password: conn.config.password || "", database: "", // No db selected useSSH: conn.config.useSSH || false, ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" } }; const res = await CreateDatabase(config as any, values.name); if (res.success) { message.success("数据库创建成功"); setIsCreateDbModalOpen(false); createDbForm.resetFields(); // Refresh node loadDatabases(targetConnection); } else { message.error("创建失败: " + res.message); } } catch (e) { // Validate failed } }; const onSearch = (e: React.ChangeEvent) => { const { value } = e.target; setSearchValue(value); }; const loop = (data: TreeNode[]): TreeNode[] => { const result: TreeNode[] = []; data.forEach(item => { const match = item.title.toLowerCase().indexOf(searchValue.toLowerCase()) > -1; if (item.children) { const filteredChildren = loop(item.children); if (filteredChildren.length > 0 || match) { result.push({ ...item, children: filteredChildren }); } } else { if (match) { result.push(item); } } }); return result; }; const displayTreeData = useMemo(() => { if (!searchValue) return treeData; return loop(treeData); }, [searchValue, treeData]); const titleRender = (node: any) => { if (node.type === 'connection') { const items: MenuProps['items'] = [ { key: 'new-db', label: '新建数据库', icon: , onClick: () => { setTargetConnection(node); setIsCreateDbModalOpen(true); } }, { key: 'refresh', label: '刷新', icon: , onClick: () => loadDatabases(node) }, { type: 'divider' }, { key: 'new-query', label: '新建查询', icon: , onClick: () => { addTab({ id: `query-${Date.now()}`, title: `新建查询`, type: 'query', connectionId: node.key, dbName: undefined }); } }, ]; return ( {node.title} ); } else if (node.type === 'database') { const items: MenuProps['items'] = [ { key: 'new-table', label: '新建表', icon: , onClick: () => openNewTableDesign(node) }, { key: 'refresh', label: '刷新', icon: , onClick: () => loadTables(node) }, { type: 'divider' }, { key: 'new-query', label: '新建查询', icon: , onClick: () => { addTab({ id: `query-${Date.now()}`, title: `新建查询 (${node.title})`, type: 'query', connectionId: node.dataRef.id, dbName: node.title }); } }, { key: 'run-sql', label: '运行 SQL 文件...', icon: , onClick: () => handleRunSQLFile(node) } ]; return ( {node.title} ); } else if (node.type === 'table') { const contextMenu: MenuProps['items'] = [ { key: 'design-table', label: '设计表', icon: , onClick: () => openDesign(node, 'columns', false) }, { key: 'copy-structure', label: '复制表结构', icon: , onClick: () => handleCopyStructure(node) }, { key: 'backup-table', label: '备份表 (SQL)', icon: , onClick: () => handleExport(node, 'sql') }, { type: 'divider' }, { key: 'export', label: '导出表数据', icon: , children: [ { key: 'export-csv', label: '导出 CSV', onClick: () => handleExport(node, 'csv') }, { key: 'export-xlsx', label: '导出 Excel (XLSX)', onClick: () => handleExport(node, 'xlsx') }, { key: 'export-json', label: '导出 JSON', onClick: () => handleExport(node, 'json') }, { key: 'export-md', label: '导出 Markdown', onClick: () => handleExport(node, 'md') }, ] } ]; return ( {node.title} ); } return {node.title}; }; return (
setIsCreateDbModalOpen(false)} >
{/* Charset option could be added here */}
); }; export default Sidebar;