diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 012f3da..95c7055 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -66,6 +66,7 @@ const readSidebarSource = () => [ readSourceFile('./sidebar/SidebarSearchPanel.tsx'), readSourceFile('./sidebar/sidebarLegacyNodeMenu.tsx'), readSourceFile('./sidebar/sidebarMetadataLoaders.ts'), + readSourceFile('./sidebar/useSidebarBatchExport.ts'), readSourceFile('./sidebarV2Utils.ts'), ].join('\n'); const readLegacyNodeMenuSource = () => readSourceFile('./sidebar/sidebarLegacyNodeMenu.tsx'); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 662bca4..0c4802b 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -26,6 +26,11 @@ import { splitQualifiedName, supportsDatabaseEvents, } from './sidebar/sidebarMetadataLoaders'; +import { + useSidebarBatchExport, + type BatchObjectFilterType, + type BatchSelectionScope, +} from './sidebar/useSidebarBatchExport'; import { V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID, formatSidebarRowCount, @@ -352,11 +357,6 @@ const buildConnectionRootRedisCommandTabTitle = (redisDbLabel = 'db0') => const buildConnectionRootRedisMonitorTabTitle = (redisDbLabel = 'db0') => t('sidebar.tab.redis_monitor', { database: redisDbLabel }); -type BatchTableExportMode = 'schema' | 'backup' | 'dataOnly'; -type BatchObjectType = 'table' | 'view'; -type BatchObjectFilterType = 'all' | BatchObjectType; -type BatchSelectionScope = 'filtered' | 'all'; - const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; labelKey: string }> = [ { key: 'all', labelKey: 'sidebar.command_search.object_kind.all' }, { key: 'tables', labelKey: 'sidebar.command_search.object_kind.tables' }, @@ -371,14 +371,6 @@ type SidebarMessagePublishTarget = { destination: string; }; -interface BatchObjectItem { - title: string; - key: string; - objectName: string; - objectType: BatchObjectType; - dataRef: any; -} - type DriverStatusSnapshot = { type: string; name: string; @@ -962,63 +954,52 @@ const Sidebar: React.FC<{ const [isCreateTagModalOpen, setIsCreateTagModalOpen] = useState(false); const [createTagForm] = Form.useForm(); - // Batch Operations Modal - const [isBatchModalOpen, setIsBatchModalOpen] = useState(false); - const [batchTables, setBatchTables] = useState([]); - const [checkedTableKeys, setCheckedTableKeys] = useState([]); - const [batchDbContext, setBatchDbContext] = useState(null); - const [selectedConnection, setSelectedConnection] = useState(''); - const [selectedDatabase, setSelectedDatabase] = useState(''); - const [availableDatabases, setAvailableDatabases] = useState([]); - const [batchFilterKeyword, setBatchFilterKeyword] = useState(''); - const [batchFilterType, setBatchFilterType] = useState('all'); - const [batchSelectionScope, setBatchSelectionScope] = useState('filtered'); - const filteredBatchObjects = useMemo(() => { - const keyword = batchFilterKeyword.trim().toLowerCase(); - return batchTables.filter((item) => { - if (batchFilterType !== 'all' && item.objectType !== batchFilterType) { - return false; - } - if (!keyword) { - return true; - } - return item.title.toLowerCase().includes(keyword) || item.objectName.toLowerCase().includes(keyword); - }); - }, [batchFilterKeyword, batchFilterType, batchTables]); - const groupedBatchObjects = useMemo(() => { - const tables = filteredBatchObjects.filter(item => item.objectType === 'table'); - const views = filteredBatchObjects.filter(item => item.objectType === 'view'); - return { tables, views }; - }, [filteredBatchObjects]); - const allBatchObjectKeys = useMemo(() => batchTables.map(item => item.key), [batchTables]); - const allBatchObjectKeysByType = useMemo(() => { - if (batchFilterType === 'all') { - return allBatchObjectKeys; - } - return batchTables - .filter((item) => item.objectType === batchFilterType) - .map((item) => item.key); - }, [allBatchObjectKeys, batchFilterType, batchTables]); - const filteredBatchObjectKeys = useMemo(() => filteredBatchObjects.map(item => item.key), [filteredBatchObjects]); - const selectionScopeTargetKeys = useMemo( - () => (batchSelectionScope === 'filtered' ? filteredBatchObjectKeys : allBatchObjectKeysByType), - [allBatchObjectKeysByType, batchSelectionScope, filteredBatchObjectKeys] - ); - useEffect(() => { - if (batchFilterType === 'all') { - return; - } - const allowed = new Set(allBatchObjectKeysByType); - setCheckedTableKeys((prev) => prev.filter((key) => allowed.has(key))); - }, [allBatchObjectKeysByType, batchFilterType]); - - // Batch Database Operations Modal - const [isBatchDbModalOpen, setIsBatchDbModalOpen] = useState(false); - const [batchDatabases, setBatchDatabases] = useState([]); - const [checkedDbKeys, setCheckedDbKeys] = useState([]); - const [batchConnContext, setBatchConnContext] = useState(null); - const [selectedDbConnection, setSelectedDbConnection] = useState(''); - + const { + isBatchModalOpen, + setIsBatchModalOpen, + batchTables, + checkedTableKeys, + setCheckedTableKeys, + selectedConnection, + selectedDatabase, + availableDatabases, + batchFilterKeyword, + setBatchFilterKeyword, + batchFilterType, + setBatchFilterType, + batchSelectionScope, + setBatchSelectionScope, + filteredBatchObjects, + groupedBatchObjects, + selectionScopeTargetKeys, + isBatchDbModalOpen, + setIsBatchDbModalOpen, + batchDatabases, + checkedDbKeys, + setCheckedDbKeys, + selectedDbConnection, + handleExportDatabaseSQL, + handleExportSchemaSQL, + openBatchOperationModal, + openBatchTableExportWorkbench, + handleConnectionChange, + handleDatabaseChange, + handleBatchExport, + handleBatchClear, + handleCheckAll, + handleInvertSelection, + openBatchDatabaseModal, + openBatchDatabaseExportWorkbench, + handleDbConnectionChange, + handleBatchDbExport, + handleCheckAllDb, + handleInvertSelectionDb, + } = useSidebarBatchExport({ + connections, + selectedNodesRef, + addTab, + addSqlLog, + }); // Find in Database Modal const [findInDbContext, setFindInDbContext] = useState<{ open: boolean; connectionId: string; dbName: string }>({ open: false, connectionId: '', dbName: '' }); @@ -2668,595 +2649,6 @@ const Sidebar: React.FC<{ }, wasClosed ? 350 : 0); }; - const normalizeConnConfig = (raw: any) => ( - buildRpcConnectionConfig(raw) - ); - - const handleExportDatabaseSQL = async (node: any, includeData: boolean) => { - const conn = node.dataRef; - const dbName = conn.dbName || node.title; - const hide = message.loading( - includeData - ? t('sidebar.message.exporting_database_backup', { database: dbName }) - : t('sidebar.message.exporting_database_schema', { database: dbName }), - 0, - ); - try { - const res = await (window as any).go.app.App.ExportDatabaseSQL(normalizeConnConfig(conn.config), dbName, includeData); - hide(); - if (res.success) { - message.success(t('sidebar.message.export_success')); - } else if (res.message !== '已取消') { - message.error(t('sidebar.message.export_failed', { error: res.message })); - } - } catch (e: any) { - hide(); - message.error(t('sidebar.message.export_failed', { error: e?.message || String(e) })); - } - }; - - const handleExportSchemaSQL = async (node: any, includeData: boolean) => { - const conn = node?.dataRef; - const dbName = String(conn?.dbName || '').trim(); - const schemaName = String(conn?.schemaName || '').trim(); - if (!conn || !dbName || !schemaName) { - message.error(t('sidebar.message.schema_export_target_missing')); - return; - } - const hide = message.loading( - includeData - ? t('sidebar.message.exporting_schema_backup', { schema: schemaName }) - : t('sidebar.message.exporting_schema_structure', { schema: schemaName }), - 0, - ); - try { - const res = await (window as any).go.app.App.ExportSchemaSQL( - buildRpcConnectionConfig(conn.config, { database: dbName }) as any, - dbName, - schemaName, - includeData, - ); - hide(); - if (res.success) { - message.success(t('sidebar.message.export_success')); - } else if (res.message !== '已取消') { - message.error(t('sidebar.message.export_failed', { error: res.message })); - } - } catch (e: any) { - hide(); - message.error(t('sidebar.message.export_failed', { error: e?.message || String(e) })); - } - }; - - const handleExportTablesSQL = async (nodes: any[], includeData: boolean) => { - if (!nodes || nodes.length === 0) return; - const first = nodes[0].dataRef; - const dbName = first.dbName; - const connId = first.id; - const allSame = nodes.every(n => n?.dataRef?.id === connId && n?.dataRef?.dbName === dbName); - if (!allSame) { - message.error(t('sidebar.message.export_tables_same_database_required')); - return; - } - - const tableNames = nodes.map(n => n.dataRef.tableName).filter(Boolean); - const hide = message.loading( - includeData - ? t('sidebar.message.backing_up_selected_tables', { count: tableNames.length }) - : t('sidebar.message.exporting_selected_table_schema', { count: tableNames.length }), - 0, - ); - try { - const res = await (window as any).go.app.App.ExportTablesSQL(normalizeConnConfig(first.config), dbName, tableNames, includeData); - hide(); - if (res.success) { - message.success(t('sidebar.message.export_success')); - } else if (res.message !== '已取消') { - message.error(t('sidebar.message.export_failed', { error: res.message })); - } - } catch (e: any) { - hide(); - message.error(t('sidebar.message.export_failed', { error: e?.message || String(e) })); - } - }; - - const openBatchOperationModal = async () => { - // Check if current selected node is database or table - let connId = ''; - let dbName = ''; - - if (selectedNodesRef.current.length > 0) { - const node = selectedNodesRef.current[0]; - if (node.type === 'database') { - connId = node.dataRef.id; - dbName = node.title; - } else if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') { - connId = node.dataRef.id; - dbName = node.dataRef.dbName; - } - } - - setSelectedConnection(connId); - setSelectedDatabase(dbName); - setBatchTables([]); - setCheckedTableKeys([]); - setAvailableDatabases([]); - setBatchFilterKeyword(''); - setBatchFilterType('all'); - setBatchSelectionScope('filtered'); - - if (connId) { - const conn = connections.find(c => c.id === connId); - if (conn) { - await loadDatabasesForBatch(conn); - if (dbName) { - await loadTablesForBatch(conn, dbName); - } - } - } - - setIsBatchModalOpen(true); - }; - - const openBatchTableExportWorkbench = () => { - let connId = ''; - let dbName = ''; - - if (selectedNodesRef.current.length > 0) { - const node = selectedNodesRef.current[0]; - if (node.type === 'connection' && node.dataRef?.config?.type !== 'redis') { - connId = node.key as string; - } else if (node.type === 'database') { - connId = node.dataRef.id; - dbName = node.title; - } else if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') { - connId = node.dataRef.id; - dbName = node.dataRef.dbName; - } - } - - addTab(buildBatchTableExportWorkbenchTab({ - connectionId: connId, - dbName: dbName || undefined, - title: dbName ? `批量导出 ${dbName} 对象` : '批量导出对象', - })); - }; - - const loadDatabasesForBatch = async (conn: 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 DBGetDatabases(buildRpcConnectionConfig(config) as any); - if (res.success) { - const dbRows: any[] = Array.isArray(res.data) ? res.data : []; - let dbs = dbRows.map((row: any) => { - const dbName = row.Database || row.database; - return { - title: dbName, - key: `${conn.id}-${dbName}`, - dbName: dbName - }; - }); - - if (conn.includeDatabases && conn.includeDatabases.length > 0) { - dbs = dbs.filter(db => conn.includeDatabases!.includes(db.dbName)); - } - - setAvailableDatabases(dbs); - } else { - message.error(t('sidebar.message.load_database_list_failed', { error: res.message })); - } - }; - - const loadTablesForBatch = async (conn: SavedConnection, dbName: string) => { - setBatchDbContext({ conn, dbName }); - - 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, viewResult] = await Promise.all([ - DBGetTables(buildRpcConnectionConfig(config) as any, dbName), - loadViews(conn, dbName).catch(() => ({ views: [], supported: false })), - ]); - - if (!res.success) { - message.error(t('sidebar.message.load_table_list_failed', { error: res.message })); - return; - } - - const tableRows: any[] = Array.isArray(res.data) ? res.data : []; - const viewRows: SidebarViewMetadataEntry[] = Array.isArray(viewResult.views) ? viewResult.views : []; - const viewSet = new Set( - viewRows.flatMap((view) => { - const names = [view.viewName.toLowerCase()]; - if (view.schemaName && !view.viewName.includes('.')) { - names.push(`${view.schemaName}.${view.viewName}`.toLowerCase()); - } - return names; - }) - ); - - const tableObjects: BatchObjectItem[] = tableRows - .map((row: any) => Object.values(row)[0] as string) - .filter((tableName: string) => !viewSet.has(tableName.toLowerCase())) - .map((tableName: string) => ({ - title: getSidebarTableDisplayName(conn, tableName), - key: `${conn.id}-${dbName}-table-${tableName}`, - objectName: tableName, - objectType: 'table' as const, - dataRef: { ...conn, tableName, dbName, objectType: 'table' }, - })); - - const viewObjects: BatchObjectItem[] = viewRows.map((view) => { - const keyName = buildSidebarObjectKeyName(dbName, view.schemaName, view.viewName); - return { - title: getSidebarTableDisplayName(conn, view.viewName), - key: `${conn.id}-${dbName}-view-${keyName}`, - objectName: view.viewName, - objectType: 'view' as const, - dataRef: { ...conn, tableName: view.viewName, schemaName: view.schemaName, dbName, objectType: 'view' }, - }; - }); - - tableObjects.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); - viewObjects.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); - - setBatchTables([...tableObjects, ...viewObjects]); - setCheckedTableKeys([]); - }; - - const handleConnectionChange = async (connId: string) => { - setSelectedConnection(connId); - setSelectedDatabase(''); - setBatchTables([]); - setCheckedTableKeys([]); - setBatchFilterKeyword(''); - setBatchFilterType('all'); - setBatchSelectionScope('filtered'); - - const conn = connections.find(c => c.id === connId); - if (conn) { - await loadDatabasesForBatch(conn); - } - }; - - const handleDatabaseChange = async (dbName: string) => { - setSelectedDatabase(dbName); - setBatchFilterKeyword(''); - setBatchFilterType('all'); - setBatchSelectionScope('filtered'); - - const conn = connections.find(c => c.id === selectedConnection); - if (conn && dbName) { - await loadTablesForBatch(conn, dbName); - } - }; - - const handleBatchExport = async (mode: BatchTableExportMode) => { - const selectedObjects = batchTables.filter(t => checkedTableKeys.includes(t.key)); - if (selectedObjects.length === 0) { - message.warning(t('sidebar.message.select_object_required')); - return; - } - - setIsBatchModalOpen(false); - - const { conn, dbName } = batchDbContext; - const objectNames = selectedObjects.map(t => t.objectName); - const selectedViewCount = selectedObjects.filter(item => item.objectType === 'view').length; - - const loadingText = mode === 'backup' - ? t('sidebar.message.backing_up_selected_objects', { count: objectNames.length }) - : mode === 'dataOnly' - ? t('sidebar.message.exporting_selected_object_data', { count: objectNames.length, format: 'INSERT' }) - : t('sidebar.message.exporting_selected_object_schema', { count: objectNames.length }); - const hide = message.loading(loadingText, 0); - try { - const app = (window as any).go.app.App; - const res = mode === 'dataOnly' - ? await app.ExportTablesDataSQL(normalizeConnConfig(conn.config), dbName, objectNames) - : await app.ExportTablesSQL(normalizeConnConfig(conn.config), dbName, objectNames, mode === 'backup'); - hide(); - if (res.success) { - if (mode !== 'schema' && selectedViewCount > 0) { - message.success(t('sidebar.message.export_success_skipped_views', { count: selectedViewCount })); - } else { - message.success(t('sidebar.message.export_success')); - } - } else if (res.message !== '已取消') { - message.error(t('sidebar.message.export_failed', { error: res.message })); - } - } catch (e: any) { - hide(); - message.error(t('sidebar.message.export_failed', { error: e?.message || String(e) })); - } - }; - - const handleBatchClear = async () => { - const selectedObjects = batchTables.filter(t => checkedTableKeys.includes(t.key)); - if (selectedObjects.length === 0) { - message.warning(t('sidebar.message.select_object_required')); - return; - } - - const { conn, dbName } = batchDbContext; - const objectNames = selectedObjects.map(t => t.objectName); - - const ok = await new Promise((resolve) => { - Modal.confirm({ - title: t('sidebar.modal.confirm_clear_selected_tables.title'), - content: t('sidebar.modal.confirm_clear_selected_tables.content', { - connection: conn.name, - database: dbName, - }), - okText: t('sidebar.action.continue'), - cancelText: t('sidebar.action.cancel'), - onOk: () => resolve(true), - onCancel: () => resolve(false), - }); - }); - if (!ok) return; - - setIsBatchModalOpen(false); - const hide = message.loading(t('sidebar.message.clearing_selected_tables', { count: objectNames.length }), 0); - const startTime = Date.now(); - try { - const app = (window as any).go.app.App; - const res = await app.ClearTables(normalizeConnConfig(conn.config), dbName, objectNames); - hide(); - const duration = Date.now() - startTime; - if (res.success) { - message.success(t('sidebar.message.clear_success')); - // 构造 SQL 日志 - let logSql = `/* Clear Tables (${objectNames.length} tables) */\n`; - if (res.data && res.data.executedSQLs && Array.isArray(res.data.executedSQLs)) { - logSql += res.data.executedSQLs.join(';\n') + ';'; - } else { - logSql += objectNames.map(name => name).join('; '); - } - addSqlLog({ - id: Date.now().toString(), - timestamp: Date.now(), - sql: logSql, - status: 'success', - duration, - message: res.message, - dbName, - affectedRows: res.data?.count || 0 - }); - } else if (res.message !== '已取消') { - message.error(t('sidebar.message.clear_failed', { error: res.message })); - // 记录失败的日志 - let logSql = `/* Clear Tables (${objectNames.length} tables) - FAILED */\n`; - if (res.data && res.data.executedSQLs && Array.isArray(res.data.executedSQLs)) { - logSql += res.data.executedSQLs.join(';\n') + ';'; - } else { - logSql += objectNames.map(name => name).join('; '); - } - addSqlLog({ - id: Date.now().toString(), - timestamp: Date.now(), - sql: logSql, - status: 'error', - duration, - message: res.message, - dbName - }); - } - } catch (e: any) { - const duration = Date.now() - startTime; - hide(); - const errMsg = e?.message || String(e); - message.error(t('sidebar.message.clear_failed', { error: errMsg })); - // 记录异常的日志 - let logSql = `/* Clear Tables (${objectNames.length} tables) - ERROR */\n`; - logSql += objectNames.map(name => name).join('; '); - addSqlLog({ - id: Date.now().toString(), - timestamp: Date.now(), - sql: logSql, - status: 'error', - duration, - message: errMsg, - dbName - }); - } - }; - - const handleCheckAll = (checked: boolean) => { - if (batchSelectionScope === 'all') { - setCheckedTableKeys(checked ? allBatchObjectKeys : []); - return; - } - if (filteredBatchObjectKeys.length === 0) { - return; - } - if (checked) { - setCheckedTableKeys(prev => { - const nextSet = new Set(prev); - filteredBatchObjectKeys.forEach((key) => nextSet.add(key)); - return allBatchObjectKeys.filter((key) => nextSet.has(key)); - }); - return; - } - const filteredKeySet = new Set(filteredBatchObjectKeys); - setCheckedTableKeys(prev => prev.filter((key) => !filteredKeySet.has(key))); - }; - - const handleInvertSelection = () => { - if (batchSelectionScope === 'all') { - setCheckedTableKeys(prev => allBatchObjectKeys.filter((key) => !prev.includes(key))); - return; - } - if (filteredBatchObjectKeys.length === 0) { - return; - } - setCheckedTableKeys(prev => { - const nextSet = new Set(prev); - filteredBatchObjectKeys.forEach((key) => { - if (nextSet.has(key)) { - nextSet.delete(key); - } else { - nextSet.add(key); - } - }); - return allBatchObjectKeys.filter((key) => nextSet.has(key)); - }); - }; - - const openBatchDatabaseModal = async () => { - // Check if current selected node is connection or database - let connId = ''; - - if (selectedNodesRef.current.length > 0) { - const node = selectedNodesRef.current[0]; - if (node.type === 'connection' && node.dataRef?.config?.type !== 'redis') { - connId = node.key as string; - } else if (node.type === 'database') { - connId = node.dataRef.id; - } else if (node.type === 'table') { - connId = node.dataRef.id; - } - } - - setSelectedDbConnection(connId); - setBatchDatabases([]); - setCheckedDbKeys([]); - - if (connId) { - const conn = connections.find(c => c.id === connId); - if (conn) { - await loadDatabasesForDbBatch(conn); - } - } - - setIsBatchDbModalOpen(true); - }; - - const openBatchDatabaseExportWorkbench = () => { - let connId = ''; - - if (selectedNodesRef.current.length > 0) { - const node = selectedNodesRef.current[0]; - if (node.type === 'connection' && node.dataRef?.config?.type !== 'redis') { - connId = node.key as string; - } else if (node.type === 'database' || node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') { - connId = node.dataRef.id; - } - } - - addTab(buildBatchDatabaseExportWorkbenchTab({ - connectionId: connId, - title: '批量导出库', - })); - }; - - const loadDatabasesForDbBatch = async (conn: SavedConnection) => { - setBatchConnContext(conn); - - 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 DBGetDatabases(buildRpcConnectionConfig(config) as any); - if (res.success) { - const dbRows: any[] = Array.isArray(res.data) ? res.data : []; - let dbs = dbRows.map((row: any) => { - const dbName = row.Database || row.database; - return { - title: dbName, - key: `${conn.id}-${dbName}`, - dbName: dbName, - dataRef: { ...conn, dbName } - }; - }); - - if (conn.includeDatabases && conn.includeDatabases.length > 0) { - dbs = dbs.filter(db => conn.includeDatabases!.includes(db.dbName)); - } - - setBatchDatabases(dbs); - setCheckedDbKeys([]); - } else { - message.error(t('sidebar.message.load_database_list_failed', { error: res.message })); - } - }; - - const handleDbConnectionChange = async (connId: string) => { - setSelectedDbConnection(connId); - - const conn = connections.find(c => c.id === connId); - if (conn) { - await loadDatabasesForDbBatch(conn); - } - }; - - const handleBatchDbExport = async (includeData: boolean) => { - const selectedDbs = batchDatabases.filter(db => checkedDbKeys.includes(db.key)); - if (selectedDbs.length === 0) { - message.warning(t('sidebar.message.select_database_required')); - return; - } - - setIsBatchDbModalOpen(false); - - for (const db of selectedDbs) { - const hide = message.loading( - includeData - ? t('sidebar.message.exporting_database_backup', { database: db.dbName }) - : t('sidebar.message.exporting_database_schema', { database: db.dbName }), - 0, - ); - try { - const res = await (window as any).go.app.App.ExportDatabaseSQL(normalizeConnConfig(batchConnContext.config), db.dbName, includeData); - hide(); - if (res.success) { - message.success(t('sidebar.message.database_export_success', { database: db.dbName })); - } else if (res.message !== '已取消') { - message.error(t('sidebar.message.database_export_failed', { database: db.dbName, error: res.message })); - break; - } else { - break; // User cancelled - } - } catch (e: any) { - hide(); - message.error(t('sidebar.message.database_export_failed', { database: db.dbName, error: e?.message || String(e) })); - break; - } - } - }; - - const handleCheckAllDb = (checked: boolean) => { - if (checked) { - setCheckedDbKeys(batchDatabases.map(db => db.key)); - } else { - setCheckedDbKeys([]); - } - }; - - const handleInvertSelectionDb = () => { - const allKeys = batchDatabases.map(db => db.key); - const newChecked = allKeys.filter(k => !checkedDbKeys.includes(k)); - setCheckedDbKeys(newChecked); - }; - const handleRunSQLFile = async (node: any) => { const res = await OpenSQLFile(); if (res.success) { diff --git a/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts b/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts index fe8bcc3..4c9b669 100644 --- a/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts +++ b/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); +const source = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8'); const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const; const requiredKeys = [ 'sidebar.message.select_object_required', diff --git a/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts b/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts index ba20716..320c19c 100644 --- a/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts +++ b/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); +const source = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8'); const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const; const requiredKeys = [ 'sidebar.message.load_database_list_failed', diff --git a/frontend/src/components/SidebarBatchObjectExportFeedback.i18n.test.ts b/frontend/src/components/SidebarBatchObjectExportFeedback.i18n.test.ts index 9f6c00d..47df65b 100644 --- a/frontend/src/components/SidebarBatchObjectExportFeedback.i18n.test.ts +++ b/frontend/src/components/SidebarBatchObjectExportFeedback.i18n.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); +const source = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8'); const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const; const requiredKeys = [ 'sidebar.message.select_object_required', diff --git a/frontend/src/components/SidebarBatchObjectLoadFeedback.i18n.test.ts b/frontend/src/components/SidebarBatchObjectLoadFeedback.i18n.test.ts index ccee574..e05c56c 100644 --- a/frontend/src/components/SidebarBatchObjectLoadFeedback.i18n.test.ts +++ b/frontend/src/components/SidebarBatchObjectLoadFeedback.i18n.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); +const source = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8'); const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const; const requiredKeys = [ 'sidebar.message.load_database_list_failed', diff --git a/frontend/src/components/SidebarSchemaExportFeedback.i18n.test.ts b/frontend/src/components/SidebarSchemaExportFeedback.i18n.test.ts index 054a5e2..9832640 100644 --- a/frontend/src/components/SidebarSchemaExportFeedback.i18n.test.ts +++ b/frontend/src/components/SidebarSchemaExportFeedback.i18n.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); +const source = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8'); const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const; const requiredKeys = [ 'sidebar.message.schema_export_target_missing', diff --git a/frontend/src/components/SidebarTablesExportFeedback.i18n.test.ts b/frontend/src/components/SidebarTablesExportFeedback.i18n.test.ts index fa568ce..ca16ecb 100644 --- a/frontend/src/components/SidebarTablesExportFeedback.i18n.test.ts +++ b/frontend/src/components/SidebarTablesExportFeedback.i18n.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); +const source = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8'); const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const; const requiredKeys = [ 'sidebar.message.export_tables_same_database_required', diff --git a/frontend/src/components/sidebar/useSidebarBatchExport.ts b/frontend/src/components/sidebar/useSidebarBatchExport.ts new file mode 100644 index 0000000..dcab1ec --- /dev/null +++ b/frontend/src/components/sidebar/useSidebarBatchExport.ts @@ -0,0 +1,734 @@ +import { useEffect, useMemo, useState, type MutableRefObject } from 'react'; +import { Modal, message } from 'antd'; + +import { DBGetDatabases, DBGetTables } from '../../../wailsjs/go/app/App'; +import type { SavedConnection } from '../../types'; +import { t } from '../../i18n'; +import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig'; +import type { SidebarViewMetadataEntry } from '../../utils/sidebarMetadata'; +import { + buildBatchDatabaseExportWorkbenchTab, + buildBatchTableExportWorkbenchTab, +} from '../../utils/tableExportTab'; +import { + buildSidebarObjectKeyName, + getMetadataDialect, + getSidebarTableDisplayName, + loadViews, +} from './sidebarMetadataLoaders'; + +export type BatchTableExportMode = 'schema' | 'backup' | 'dataOnly'; +export type BatchObjectType = 'table' | 'view'; +export type BatchObjectFilterType = 'all' | BatchObjectType; +export type BatchSelectionScope = 'filtered' | 'all'; + +export interface BatchObjectItem { + title: string; + key: string; + objectName: string; + objectType: BatchObjectType; + dataRef: any; +} + +interface UseSidebarBatchExportArgs { + connections: SavedConnection[]; + selectedNodesRef: MutableRefObject; + addTab: (tab: any) => void; + addSqlLog: (log: any) => void; +} + +export const useSidebarBatchExport = ({ + connections, + selectedNodesRef, + addTab, + addSqlLog, +}: UseSidebarBatchExportArgs) => { + // Batch Operations Modal + const [isBatchModalOpen, setIsBatchModalOpen] = useState(false); + const [batchTables, setBatchTables] = useState([]); + const [checkedTableKeys, setCheckedTableKeys] = useState([]); + const [batchDbContext, setBatchDbContext] = useState(null); + const [selectedConnection, setSelectedConnection] = useState(''); + const [selectedDatabase, setSelectedDatabase] = useState(''); + const [availableDatabases, setAvailableDatabases] = useState([]); + const [batchFilterKeyword, setBatchFilterKeyword] = useState(''); + const [batchFilterType, setBatchFilterType] = useState('all'); + const [batchSelectionScope, setBatchSelectionScope] = useState('filtered'); + const filteredBatchObjects = useMemo(() => { + const keyword = batchFilterKeyword.trim().toLowerCase(); + return batchTables.filter((item) => { + if (batchFilterType !== 'all' && item.objectType !== batchFilterType) { + return false; + } + if (!keyword) { + return true; + } + return item.title.toLowerCase().includes(keyword) || item.objectName.toLowerCase().includes(keyword); + }); + }, [batchFilterKeyword, batchFilterType, batchTables]); + const groupedBatchObjects = useMemo(() => { + const tables = filteredBatchObjects.filter(item => item.objectType === 'table'); + const views = filteredBatchObjects.filter(item => item.objectType === 'view'); + return { tables, views }; + }, [filteredBatchObjects]); + const allBatchObjectKeys = useMemo(() => batchTables.map(item => item.key), [batchTables]); + const allBatchObjectKeysByType = useMemo(() => { + if (batchFilterType === 'all') { + return allBatchObjectKeys; + } + return batchTables + .filter((item) => item.objectType === batchFilterType) + .map((item) => item.key); + }, [allBatchObjectKeys, batchFilterType, batchTables]); + const filteredBatchObjectKeys = useMemo(() => filteredBatchObjects.map(item => item.key), [filteredBatchObjects]); + const selectionScopeTargetKeys = useMemo( + () => (batchSelectionScope === 'filtered' ? filteredBatchObjectKeys : allBatchObjectKeysByType), + [allBatchObjectKeysByType, batchSelectionScope, filteredBatchObjectKeys] + ); + useEffect(() => { + if (batchFilterType === 'all') { + return; + } + const allowed = new Set(allBatchObjectKeysByType); + setCheckedTableKeys((prev) => prev.filter((key) => allowed.has(key))); + }, [allBatchObjectKeysByType, batchFilterType]); + + // Batch Database Operations Modal + const [isBatchDbModalOpen, setIsBatchDbModalOpen] = useState(false); + const [batchDatabases, setBatchDatabases] = useState([]); + const [checkedDbKeys, setCheckedDbKeys] = useState([]); + const [batchConnContext, setBatchConnContext] = useState(null); + const [selectedDbConnection, setSelectedDbConnection] = useState(''); + + + const normalizeConnConfig = (raw: any) => ( + buildRpcConnectionConfig(raw) + ); + + const handleExportDatabaseSQL = async (node: any, includeData: boolean) => { + const conn = node.dataRef; + const dbName = conn.dbName || node.title; + const hide = message.loading( + includeData + ? t('sidebar.message.exporting_database_backup', { database: dbName }) + : t('sidebar.message.exporting_database_schema', { database: dbName }), + 0, + ); + try { + const res = await (window as any).go.app.App.ExportDatabaseSQL(normalizeConnConfig(conn.config), dbName, includeData); + hide(); + if (res.success) { + message.success(t('sidebar.message.export_success')); + } else if (res.message !== '已取消') { + message.error(t('sidebar.message.export_failed', { error: res.message })); + } + } catch (e: any) { + hide(); + message.error(t('sidebar.message.export_failed', { error: e?.message || String(e) })); + } + }; + + const handleExportSchemaSQL = async (node: any, includeData: boolean) => { + const conn = node?.dataRef; + const dbName = String(conn?.dbName || '').trim(); + const schemaName = String(conn?.schemaName || '').trim(); + if (!conn || !dbName || !schemaName) { + message.error(t('sidebar.message.schema_export_target_missing')); + return; + } + const hide = message.loading( + includeData + ? t('sidebar.message.exporting_schema_backup', { schema: schemaName }) + : t('sidebar.message.exporting_schema_structure', { schema: schemaName }), + 0, + ); + try { + const res = await (window as any).go.app.App.ExportSchemaSQL( + buildRpcConnectionConfig(conn.config, { database: dbName }) as any, + dbName, + schemaName, + includeData, + ); + hide(); + if (res.success) { + message.success(t('sidebar.message.export_success')); + } else if (res.message !== '已取消') { + message.error(t('sidebar.message.export_failed', { error: res.message })); + } + } catch (e: any) { + hide(); + message.error(t('sidebar.message.export_failed', { error: e?.message || String(e) })); + } + }; + + const handleExportTablesSQL = async (nodes: any[], includeData: boolean) => { + if (!nodes || nodes.length === 0) return; + const first = nodes[0].dataRef; + const dbName = first.dbName; + const connId = first.id; + const allSame = nodes.every(n => n?.dataRef?.id === connId && n?.dataRef?.dbName === dbName); + if (!allSame) { + message.error(t('sidebar.message.export_tables_same_database_required')); + return; + } + + const tableNames = nodes.map(n => n.dataRef.tableName).filter(Boolean); + const hide = message.loading( + includeData + ? t('sidebar.message.backing_up_selected_tables', { count: tableNames.length }) + : t('sidebar.message.exporting_selected_table_schema', { count: tableNames.length }), + 0, + ); + try { + const res = await (window as any).go.app.App.ExportTablesSQL(normalizeConnConfig(first.config), dbName, tableNames, includeData); + hide(); + if (res.success) { + message.success(t('sidebar.message.export_success')); + } else if (res.message !== '已取消') { + message.error(t('sidebar.message.export_failed', { error: res.message })); + } + } catch (e: any) { + hide(); + message.error(t('sidebar.message.export_failed', { error: e?.message || String(e) })); + } + }; + + const openBatchOperationModal = async () => { + // Check if current selected node is database or table + let connId = ''; + let dbName = ''; + + if (selectedNodesRef.current.length > 0) { + const node = selectedNodesRef.current[0]; + if (node.type === 'database') { + connId = node.dataRef.id; + dbName = node.title; + } else if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') { + connId = node.dataRef.id; + dbName = node.dataRef.dbName; + } + } + + setSelectedConnection(connId); + setSelectedDatabase(dbName); + setBatchTables([]); + setCheckedTableKeys([]); + setAvailableDatabases([]); + setBatchFilterKeyword(''); + setBatchFilterType('all'); + setBatchSelectionScope('filtered'); + + if (connId) { + const conn = connections.find(c => c.id === connId); + if (conn) { + await loadDatabasesForBatch(conn); + if (dbName) { + await loadTablesForBatch(conn, dbName); + } + } + } + + setIsBatchModalOpen(true); + }; + + const openBatchTableExportWorkbench = () => { + let connId = ''; + let dbName = ''; + + if (selectedNodesRef.current.length > 0) { + const node = selectedNodesRef.current[0]; + if (node.type === 'connection' && node.dataRef?.config?.type !== 'redis') { + connId = node.key as string; + } else if (node.type === 'database') { + connId = node.dataRef.id; + dbName = node.title; + } else if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') { + connId = node.dataRef.id; + dbName = node.dataRef.dbName; + } + } + + addTab(buildBatchTableExportWorkbenchTab({ + connectionId: connId, + dbName: dbName || undefined, + title: dbName ? `批量导出 ${dbName} 对象` : '批量导出对象', + })); + }; + + const loadDatabasesForBatch = async (conn: 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 DBGetDatabases(buildRpcConnectionConfig(config) as any); + if (res.success) { + const dbRows: any[] = Array.isArray(res.data) ? res.data : []; + let dbs = dbRows.map((row: any) => { + const dbName = row.Database || row.database; + return { + title: dbName, + key: `${conn.id}-${dbName}`, + dbName: dbName + }; + }); + + if (conn.includeDatabases && conn.includeDatabases.length > 0) { + dbs = dbs.filter(db => conn.includeDatabases!.includes(db.dbName)); + } + + setAvailableDatabases(dbs); + } else { + message.error(t('sidebar.message.load_database_list_failed', { error: res.message })); + } + }; + + const loadTablesForBatch = async (conn: SavedConnection, dbName: string) => { + setBatchDbContext({ conn, dbName }); + + 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, viewResult] = await Promise.all([ + DBGetTables(buildRpcConnectionConfig(config) as any, dbName), + loadViews(conn, dbName).catch(() => ({ views: [], supported: false })), + ]); + + if (!res.success) { + message.error(t('sidebar.message.load_table_list_failed', { error: res.message })); + return; + } + + const tableRows: any[] = Array.isArray(res.data) ? res.data : []; + const viewRows: SidebarViewMetadataEntry[] = Array.isArray(viewResult.views) ? viewResult.views : []; + const viewSet = new Set( + viewRows.flatMap((view) => { + const names = [view.viewName.toLowerCase()]; + if (view.schemaName && !view.viewName.includes('.')) { + names.push(`${view.schemaName}.${view.viewName}`.toLowerCase()); + } + return names; + }) + ); + + const tableObjects: BatchObjectItem[] = tableRows + .map((row: any) => Object.values(row)[0] as string) + .filter((tableName: string) => !viewSet.has(tableName.toLowerCase())) + .map((tableName: string) => ({ + title: getSidebarTableDisplayName(conn, tableName), + key: `${conn.id}-${dbName}-table-${tableName}`, + objectName: tableName, + objectType: 'table' as const, + dataRef: { ...conn, tableName, dbName, objectType: 'table' }, + })); + + const viewObjects: BatchObjectItem[] = viewRows.map((view) => { + const keyName = buildSidebarObjectKeyName(dbName, view.schemaName, view.viewName); + return { + title: getSidebarTableDisplayName(conn, view.viewName), + key: `${conn.id}-${dbName}-view-${keyName}`, + objectName: view.viewName, + objectType: 'view' as const, + dataRef: { ...conn, tableName: view.viewName, schemaName: view.schemaName, dbName, objectType: 'view' }, + }; + }); + + tableObjects.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); + viewObjects.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); + + setBatchTables([...tableObjects, ...viewObjects]); + setCheckedTableKeys([]); + }; + + const handleConnectionChange = async (connId: string) => { + setSelectedConnection(connId); + setSelectedDatabase(''); + setBatchTables([]); + setCheckedTableKeys([]); + setBatchFilterKeyword(''); + setBatchFilterType('all'); + setBatchSelectionScope('filtered'); + + const conn = connections.find(c => c.id === connId); + if (conn) { + await loadDatabasesForBatch(conn); + } + }; + + const handleDatabaseChange = async (dbName: string) => { + setSelectedDatabase(dbName); + setBatchFilterKeyword(''); + setBatchFilterType('all'); + setBatchSelectionScope('filtered'); + + const conn = connections.find(c => c.id === selectedConnection); + if (conn && dbName) { + await loadTablesForBatch(conn, dbName); + } + }; + + const handleBatchExport = async (mode: BatchTableExportMode) => { + const selectedObjects = batchTables.filter(t => checkedTableKeys.includes(t.key)); + if (selectedObjects.length === 0) { + message.warning(t('sidebar.message.select_object_required')); + return; + } + + setIsBatchModalOpen(false); + + const { conn, dbName } = batchDbContext; + const objectNames = selectedObjects.map(t => t.objectName); + const selectedViewCount = selectedObjects.filter(item => item.objectType === 'view').length; + + const loadingText = mode === 'backup' + ? t('sidebar.message.backing_up_selected_objects', { count: objectNames.length }) + : mode === 'dataOnly' + ? t('sidebar.message.exporting_selected_object_data', { count: objectNames.length, format: 'INSERT' }) + : t('sidebar.message.exporting_selected_object_schema', { count: objectNames.length }); + const hide = message.loading(loadingText, 0); + try { + const app = (window as any).go.app.App; + const res = mode === 'dataOnly' + ? await app.ExportTablesDataSQL(normalizeConnConfig(conn.config), dbName, objectNames) + : await app.ExportTablesSQL(normalizeConnConfig(conn.config), dbName, objectNames, mode === 'backup'); + hide(); + if (res.success) { + if (mode !== 'schema' && selectedViewCount > 0) { + message.success(t('sidebar.message.export_success_skipped_views', { count: selectedViewCount })); + } else { + message.success(t('sidebar.message.export_success')); + } + } else if (res.message !== '已取消') { + message.error(t('sidebar.message.export_failed', { error: res.message })); + } + } catch (e: any) { + hide(); + message.error(t('sidebar.message.export_failed', { error: e?.message || String(e) })); + } + }; + + const handleBatchClear = async () => { + const selectedObjects = batchTables.filter(t => checkedTableKeys.includes(t.key)); + if (selectedObjects.length === 0) { + message.warning(t('sidebar.message.select_object_required')); + return; + } + + const { conn, dbName } = batchDbContext; + const objectNames = selectedObjects.map(t => t.objectName); + + const ok = await new Promise((resolve) => { + Modal.confirm({ + title: t('sidebar.modal.confirm_clear_selected_tables.title'), + content: t('sidebar.modal.confirm_clear_selected_tables.content', { + connection: conn.name, + database: dbName, + }), + okText: t('sidebar.action.continue'), + cancelText: t('sidebar.action.cancel'), + onOk: () => resolve(true), + onCancel: () => resolve(false), + }); + }); + if (!ok) return; + + setIsBatchModalOpen(false); + const hide = message.loading(t('sidebar.message.clearing_selected_tables', { count: objectNames.length }), 0); + const startTime = Date.now(); + try { + const app = (window as any).go.app.App; + const res = await app.ClearTables(normalizeConnConfig(conn.config), dbName, objectNames); + hide(); + const duration = Date.now() - startTime; + if (res.success) { + message.success(t('sidebar.message.clear_success')); + // 构造 SQL 日志 + let logSql = `/* Clear Tables (${objectNames.length} tables) */\n`; + if (res.data && res.data.executedSQLs && Array.isArray(res.data.executedSQLs)) { + logSql += res.data.executedSQLs.join(';\n') + ';'; + } else { + logSql += objectNames.map(name => name).join('; '); + } + addSqlLog({ + id: Date.now().toString(), + timestamp: Date.now(), + sql: logSql, + status: 'success', + duration, + message: res.message, + dbName, + affectedRows: res.data?.count || 0 + }); + } else if (res.message !== '已取消') { + message.error(t('sidebar.message.clear_failed', { error: res.message })); + // 记录失败的日志 + let logSql = `/* Clear Tables (${objectNames.length} tables) - FAILED */\n`; + if (res.data && res.data.executedSQLs && Array.isArray(res.data.executedSQLs)) { + logSql += res.data.executedSQLs.join(';\n') + ';'; + } else { + logSql += objectNames.map(name => name).join('; '); + } + addSqlLog({ + id: Date.now().toString(), + timestamp: Date.now(), + sql: logSql, + status: 'error', + duration, + message: res.message, + dbName + }); + } + } catch (e: any) { + const duration = Date.now() - startTime; + hide(); + const errMsg = e?.message || String(e); + message.error(t('sidebar.message.clear_failed', { error: errMsg })); + // 记录异常的日志 + let logSql = `/* Clear Tables (${objectNames.length} tables) - ERROR */\n`; + logSql += objectNames.map(name => name).join('; '); + addSqlLog({ + id: Date.now().toString(), + timestamp: Date.now(), + sql: logSql, + status: 'error', + duration, + message: errMsg, + dbName + }); + } + }; + + const handleCheckAll = (checked: boolean) => { + if (batchSelectionScope === 'all') { + setCheckedTableKeys(checked ? allBatchObjectKeys : []); + return; + } + if (filteredBatchObjectKeys.length === 0) { + return; + } + if (checked) { + setCheckedTableKeys(prev => { + const nextSet = new Set(prev); + filteredBatchObjectKeys.forEach((key) => nextSet.add(key)); + return allBatchObjectKeys.filter((key) => nextSet.has(key)); + }); + return; + } + const filteredKeySet = new Set(filteredBatchObjectKeys); + setCheckedTableKeys(prev => prev.filter((key) => !filteredKeySet.has(key))); + }; + + const handleInvertSelection = () => { + if (batchSelectionScope === 'all') { + setCheckedTableKeys(prev => allBatchObjectKeys.filter((key) => !prev.includes(key))); + return; + } + if (filteredBatchObjectKeys.length === 0) { + return; + } + setCheckedTableKeys(prev => { + const nextSet = new Set(prev); + filteredBatchObjectKeys.forEach((key) => { + if (nextSet.has(key)) { + nextSet.delete(key); + } else { + nextSet.add(key); + } + }); + return allBatchObjectKeys.filter((key) => nextSet.has(key)); + }); + }; + + const openBatchDatabaseModal = async () => { + // Check if current selected node is connection or database + let connId = ''; + + if (selectedNodesRef.current.length > 0) { + const node = selectedNodesRef.current[0]; + if (node.type === 'connection' && node.dataRef?.config?.type !== 'redis') { + connId = node.key as string; + } else if (node.type === 'database') { + connId = node.dataRef.id; + } else if (node.type === 'table') { + connId = node.dataRef.id; + } + } + + setSelectedDbConnection(connId); + setBatchDatabases([]); + setCheckedDbKeys([]); + + if (connId) { + const conn = connections.find(c => c.id === connId); + if (conn) { + await loadDatabasesForDbBatch(conn); + } + } + + setIsBatchDbModalOpen(true); + }; + + const openBatchDatabaseExportWorkbench = () => { + let connId = ''; + + if (selectedNodesRef.current.length > 0) { + const node = selectedNodesRef.current[0]; + if (node.type === 'connection' && node.dataRef?.config?.type !== 'redis') { + connId = node.key as string; + } else if (node.type === 'database' || node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') { + connId = node.dataRef.id; + } + } + + addTab(buildBatchDatabaseExportWorkbenchTab({ + connectionId: connId, + title: '批量导出库', + })); + }; + + const loadDatabasesForDbBatch = async (conn: SavedConnection) => { + setBatchConnContext(conn); + + 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 DBGetDatabases(buildRpcConnectionConfig(config) as any); + if (res.success) { + const dbRows: any[] = Array.isArray(res.data) ? res.data : []; + let dbs = dbRows.map((row: any) => { + const dbName = row.Database || row.database; + return { + title: dbName, + key: `${conn.id}-${dbName}`, + dbName: dbName, + dataRef: { ...conn, dbName } + }; + }); + + if (conn.includeDatabases && conn.includeDatabases.length > 0) { + dbs = dbs.filter(db => conn.includeDatabases!.includes(db.dbName)); + } + + setBatchDatabases(dbs); + setCheckedDbKeys([]); + } else { + message.error(t('sidebar.message.load_database_list_failed', { error: res.message })); + } + }; + + const handleDbConnectionChange = async (connId: string) => { + setSelectedDbConnection(connId); + + const conn = connections.find(c => c.id === connId); + if (conn) { + await loadDatabasesForDbBatch(conn); + } + }; + + const handleBatchDbExport = async (includeData: boolean) => { + const selectedDbs = batchDatabases.filter(db => checkedDbKeys.includes(db.key)); + if (selectedDbs.length === 0) { + message.warning(t('sidebar.message.select_database_required')); + return; + } + + setIsBatchDbModalOpen(false); + + for (const db of selectedDbs) { + const hide = message.loading( + includeData + ? t('sidebar.message.exporting_database_backup', { database: db.dbName }) + : t('sidebar.message.exporting_database_schema', { database: db.dbName }), + 0, + ); + try { + const res = await (window as any).go.app.App.ExportDatabaseSQL(normalizeConnConfig(batchConnContext.config), db.dbName, includeData); + hide(); + if (res.success) { + message.success(t('sidebar.message.database_export_success', { database: db.dbName })); + } else if (res.message !== '已取消') { + message.error(t('sidebar.message.database_export_failed', { database: db.dbName, error: res.message })); + break; + } else { + break; // User cancelled + } + } catch (e: any) { + hide(); + message.error(t('sidebar.message.database_export_failed', { database: db.dbName, error: e?.message || String(e) })); + break; + } + } + }; + + const handleCheckAllDb = (checked: boolean) => { + if (checked) { + setCheckedDbKeys(batchDatabases.map(db => db.key)); + } else { + setCheckedDbKeys([]); + } + }; + + const handleInvertSelectionDb = () => { + const allKeys = batchDatabases.map(db => db.key); + const newChecked = allKeys.filter(k => !checkedDbKeys.includes(k)); + setCheckedDbKeys(newChecked); + }; + + return { + isBatchModalOpen, + setIsBatchModalOpen, + batchTables, + checkedTableKeys, + setCheckedTableKeys, + selectedConnection, + selectedDatabase, + availableDatabases, + batchFilterKeyword, + setBatchFilterKeyword, + batchFilterType, + setBatchFilterType, + batchSelectionScope, + setBatchSelectionScope, + filteredBatchObjects, + groupedBatchObjects, + selectionScopeTargetKeys, + isBatchDbModalOpen, + setIsBatchDbModalOpen, + batchDatabases, + checkedDbKeys, + setCheckedDbKeys, + selectedDbConnection, + handleExportDatabaseSQL, + handleExportSchemaSQL, + openBatchOperationModal, + openBatchTableExportWorkbench, + handleConnectionChange, + handleDatabaseChange, + handleBatchExport, + handleBatchClear, + handleCheckAll, + handleInvertSelection, + openBatchDatabaseModal, + openBatchDatabaseExportWorkbench, + handleDbConnectionChange, + handleBatchDbExport, + handleCheckAllDb, + handleInvertSelectionDb, + }; +};