mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-22 21:27:11 +08:00
✨ feat(export-workbench): 支持批量导出工作台并优化 SQL 导出性能
- 侧边栏批量表/批量库入口改为直接打开导出工作台,统一导出配置与进度视图 - 导出工作台新增 batch-tables / batch-databases 模式,支持连接、数据库、对象选择与独立历史记录键 - 连接、数据库、对象下拉项补齐完整名展示与悬浮提示,避免长名称被截断后不可识别 - 后端新增批量对象/批量库导出 WithOptions 链路,统一返回输出文件/目录与进度信息 - SQL dump 数据导出改为按方言批量写入,MySQL/PG 等使用多值 VALUES,Oracle/达梦使用 INSERT ALL - 补充导出工作台与 SQL dump 的回归测试和 benchmark,覆盖批量模式与批量写入语义
This commit is contained in:
@@ -89,7 +89,11 @@ import { resolveConnectionAccentColor, resolveConnectionIconType } from '../util
|
||||
import { buildJVMTabTitle } from '../utils/jvmRuntimePresentation';
|
||||
import { buildJVMDiagnosticActionDescriptor, buildJVMMonitoringActionDescriptors } from '../utils/jvmSidebarActions';
|
||||
import { buildTableSelectQuery } from '../utils/objectQueryTemplates';
|
||||
import { buildTableExportTab } from '../utils/tableExportTab';
|
||||
import {
|
||||
buildBatchDatabaseExportWorkbenchTab,
|
||||
buildBatchTableExportWorkbenchTab,
|
||||
buildTableExportTab,
|
||||
} from '../utils/tableExportTab';
|
||||
import { useExportProgressDialog } from './ExportProgressModal';
|
||||
import { getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts';
|
||||
import { buildExternalSQLDirectoryId, buildExternalSQLRootNode, buildExternalSQLTabId, type ExternalSQLTreeNode } from '../utils/externalSqlTree';
|
||||
@@ -4024,6 +4028,30 @@ const Sidebar: React.FC<{
|
||||
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,
|
||||
@@ -4347,6 +4375,24 @@ const Sidebar: React.FC<{
|
||||
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);
|
||||
|
||||
@@ -9221,7 +9267,7 @@ const Sidebar: React.FC<{
|
||||
<button
|
||||
type="button"
|
||||
className="gn-v2-rail-tool gn-v2-rail-action"
|
||||
onClick={() => openBatchOperationModal()}
|
||||
onClick={() => openBatchTableExportWorkbench()}
|
||||
aria-label={v2BatchTablesLabel}
|
||||
data-sidebar-batch-table-action="true"
|
||||
>
|
||||
@@ -9232,7 +9278,7 @@ const Sidebar: React.FC<{
|
||||
<button
|
||||
type="button"
|
||||
className="gn-v2-rail-tool gn-v2-rail-action"
|
||||
onClick={() => openBatchDatabaseModal()}
|
||||
onClick={() => openBatchDatabaseExportWorkbench()}
|
||||
aria-label={v2BatchDatabasesLabel}
|
||||
data-sidebar-batch-database-action="true"
|
||||
>
|
||||
@@ -9507,7 +9553,7 @@ const Sidebar: React.FC<{
|
||||
icon={<TableOutlined />}
|
||||
aria-label="批量操作表"
|
||||
data-sidebar-batch-table-action="true"
|
||||
onClick={() => openBatchOperationModal()}
|
||||
onClick={() => openBatchTableExportWorkbench()}
|
||||
style={{ color: legacyToolbarButtonColor }}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -9520,7 +9566,7 @@ const Sidebar: React.FC<{
|
||||
icon={<DatabaseOutlined />}
|
||||
aria-label="批量操作库"
|
||||
data-sidebar-batch-database-action="true"
|
||||
onClick={() => openBatchDatabaseModal()}
|
||||
onClick={() => openBatchDatabaseExportWorkbench()}
|
||||
style={{ color: legacyToolbarButtonColor }}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import TableExportWorkbench, { buildTableExportHistoryEntry } from './TableExportWorkbench';
|
||||
import type { ExportProgressState } from './useExportProgressRunner';
|
||||
|
||||
const mockUpsertTableExportHistory = vi.fn();
|
||||
const createMockStoreState = () => ({
|
||||
@@ -24,7 +25,7 @@ const createMockStoreState = () => ({
|
||||
tableExportHistories: {},
|
||||
upsertTableExportHistory: mockUpsertTableExportHistory,
|
||||
});
|
||||
const createMockProgressRunnerState = () => ({
|
||||
const createMockProgressRunnerState = (): ExportProgressState => ({
|
||||
open: true,
|
||||
jobId: 'job-1',
|
||||
title: '导出 SYS.test',
|
||||
@@ -40,17 +41,27 @@ const createMockProgressRunnerState = () => ({
|
||||
filePath: '/Users/yangguofeng/Desktop/SYS.test.xlsx',
|
||||
message: '',
|
||||
});
|
||||
const createProgressRunnerState = (
|
||||
overrides: Partial<ExportProgressState> = {},
|
||||
): ExportProgressState => ({
|
||||
...createMockProgressRunnerState(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
let mockStoreState = createMockStoreState();
|
||||
let mockProgressRunnerState = createMockProgressRunnerState();
|
||||
let mockProgressRunnerState: ExportProgressState = createMockProgressRunnerState();
|
||||
|
||||
vi.mock('../store', () => ({
|
||||
useStore: (selector: (state: any) => any) => selector(mockStoreState),
|
||||
}));
|
||||
|
||||
vi.mock('../../wailsjs/go/app/App', () => ({
|
||||
DBGetDatabases: vi.fn(),
|
||||
DBGetTables: vi.fn(),
|
||||
ExportDatabasesSQLWithOptions: vi.fn(),
|
||||
ExportQueryWithOptions: vi.fn(),
|
||||
ExportTableWithOptions: vi.fn(),
|
||||
ExportTablesSQLWithOptions: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./useExportProgressRunner', () => ({
|
||||
@@ -167,6 +178,99 @@ describe('TableExportWorkbench', () => {
|
||||
expect(markup).toContain('/Users/yangguofeng/Desktop/SYS.test.xlsx');
|
||||
});
|
||||
|
||||
it('renders batch table workbench copy and object progress summary', () => {
|
||||
mockProgressRunnerState = createProgressRunnerState({
|
||||
title: '结构 · SYS',
|
||||
targetName: 'SYS · 8 个对象',
|
||||
format: 'SQL',
|
||||
current: 3,
|
||||
total: 8,
|
||||
totalRowsKnown: true,
|
||||
filePath: '/Users/yangguofeng/Desktop/SYS_schema_8tables.sql',
|
||||
stage: '正在导出 orders (4/8)',
|
||||
});
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<TableExportWorkbench
|
||||
tab={{
|
||||
id: 'table-export-batch-tables-conn-1-SYS',
|
||||
title: '批量导出对象',
|
||||
type: 'table-export',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'SYS',
|
||||
exportWorkbenchMode: 'batch-tables',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain('模式 · 批量对象');
|
||||
expect(markup).toContain('导出内容');
|
||||
expect(markup).toContain('批量对象导出会统一生成一个 SQL 文件');
|
||||
expect(markup).toContain('已完成 3 / 8 个对象');
|
||||
expect(markup).toContain('/Users/yangguofeng/Desktop/SYS_schema_8tables.sql');
|
||||
});
|
||||
|
||||
it('renders batch database history with directory-oriented labels', () => {
|
||||
mockProgressRunnerState = createProgressRunnerState({
|
||||
open: false,
|
||||
jobId: '',
|
||||
title: '',
|
||||
targetName: '',
|
||||
format: '',
|
||||
startedAt: 0,
|
||||
finishedAt: 0,
|
||||
status: 'idle',
|
||||
stage: '',
|
||||
current: 0,
|
||||
total: 0,
|
||||
totalRowsKnown: false,
|
||||
filePath: '',
|
||||
message: '',
|
||||
});
|
||||
mockStoreState = {
|
||||
...createMockStoreState(),
|
||||
tableExportHistories: {
|
||||
'conn-1::__batch_databases__': [
|
||||
{
|
||||
jobId: 'job-batch-db-1',
|
||||
targetName: '3 个数据库',
|
||||
startedAt: 1_000,
|
||||
finishedAt: 31_000,
|
||||
format: 'SQL',
|
||||
scope: 'selectedDatabases',
|
||||
scopeLabel: '已选数据库(3)',
|
||||
strategyLabel: '批量库 SQL 导出 · 导出库结构',
|
||||
status: 'done',
|
||||
stage: '导出完成',
|
||||
current: 3,
|
||||
total: 3,
|
||||
totalRowsKnown: true,
|
||||
filePath: '/Users/yangguofeng/Desktop/export-batch-dbs',
|
||||
message: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<TableExportWorkbench
|
||||
tab={{
|
||||
id: 'table-export-batch-databases-conn-1',
|
||||
title: '批量导出库',
|
||||
type: 'table-export',
|
||||
connectionId: 'conn-1',
|
||||
exportWorkbenchMode: 'batch-databases',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain('模式 · 批量库');
|
||||
expect(markup).toContain('将在开始导出时先选择输出目录');
|
||||
expect(markup).toContain('已完成 3 / 3 个库');
|
||||
expect(markup).toContain('/Users/yangguofeng/Desktop/export-batch-dbs');
|
||||
expect(markup).toContain('目录');
|
||||
});
|
||||
|
||||
it('keeps only one progress component in source and no longer uses top tabs', () => {
|
||||
const source = readFileSync(new URL('./TableExportWorkbench.tsx', import.meta.url), 'utf8');
|
||||
const progressMatches = source.match(/<ExportProgressBar\b/g) || [];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user