feat(data-sync/export): 支持同步与备份后台工作台

- 将数据同步及各级 SQL 备份统一迁入稳定工作区
- 全局保留任务状态、进度和结构化日志,关闭后可重新接回
- 支持从当前任务及历史记录打开 SQL 恢复工作台
- 锁定运行中配置并禁止后台任务分离到独立窗口
This commit is contained in:
Syngnat
2026-07-20 13:43:58 +08:00
parent 19d962d9ba
commit 21e0921b3b
45 changed files with 3043 additions and 647 deletions

View File

@@ -0,0 +1,38 @@
import { afterEach, describe, expect, it } from 'vitest';
import { setCurrentLanguage, t } from '../i18n';
import { getTabDisplayKindLabel } from './tabDisplay';
import {
buildDataSyncWorkbenchTab,
resolveDataSyncWorkbenchTabId,
} from './dataSyncTab';
describe('dataSyncTab', () => {
afterEach(() => {
setCurrentLanguage('zh-CN');
});
it('builds one stable workbench tab for each data workflow entry', () => {
expect(resolveDataSyncWorkbenchTabId('sync')).toBe('data-sync-workbench-sync');
expect(resolveDataSyncWorkbenchTabId('schemaCompare')).toBe('data-sync-workbench-schema-compare');
expect(resolveDataSyncWorkbenchTabId('dataCompare')).toBe('data-sync-workbench-data-compare');
expect(buildDataSyncWorkbenchTab({ entryMode: 'schemaCompare' })).toMatchObject({
id: 'data-sync-workbench-schema-compare',
title: t('data_sync.entry_mode.schema_compare.title'),
type: 'data-sync',
connectionId: '',
dataSyncEntryMode: 'schemaCompare',
});
expect(getTabDisplayKindLabel(buildDataSyncWorkbenchTab({ entryMode: 'sync' }))).toBe('SYNC');
});
it('localizes default titles without changing stable tab ids', () => {
setCurrentLanguage('en-US');
const tab = buildDataSyncWorkbenchTab({ entryMode: 'dataCompare' });
expect(tab.id).toBe('data-sync-workbench-data-compare');
expect(tab.title).toBe(t('data_sync.entry_mode.data_compare.title'));
});
});

View File

@@ -0,0 +1,36 @@
import type { TabData } from '../types';
import { t } from '../i18n';
import {
resolveDataSyncEntryModePresentation,
type DataSyncEntryMode,
} from '../components/dataSyncEntryMode';
type BuildDataSyncWorkbenchTabInput = {
entryMode: DataSyncEntryMode;
title?: string;
};
const DATA_SYNC_ENTRY_MODE_SLUGS: Record<DataSyncEntryMode, string> = {
sync: 'sync',
schemaCompare: 'schema-compare',
dataCompare: 'data-compare',
};
export const resolveDataSyncWorkbenchTabId = (
entryMode: DataSyncEntryMode,
): string => `data-sync-workbench-${DATA_SYNC_ENTRY_MODE_SLUGS[entryMode]}`;
export const buildDataSyncWorkbenchTab = (
input: BuildDataSyncWorkbenchTabInput,
): TabData => {
const presentation = resolveDataSyncEntryModePresentation(input.entryMode, t);
const title = String(input.title || presentation.title).trim() || presentation.title;
return {
id: resolveDataSyncWorkbenchTabId(input.entryMode),
title,
type: 'data-sync',
connectionId: '',
dataSyncEntryMode: input.entryMode,
};
};

View File

@@ -33,4 +33,17 @@ describe('sqlFileExecutionTab', () => {
sqlFileExecutionRequestKey: 'job-1',
}));
});
it('opens completed backup artifacts without automatically executing them', () => {
const tab = buildSQLFileExecutionWorkbenchTab({
connectionId: 'conn-1',
dbName: 'demo',
filePath: '/tmp/demo_backup.sql',
fileName: 'demo_backup.sql',
autoStart: false,
});
expect(tab.sqlFileExecutionRequestKey).toBeUndefined();
expect(tab.filePath).toBe('/tmp/demo_backup.sql');
});
});

View File

@@ -22,6 +22,7 @@ type BuildSQLFileExecutionWorkbenchTabInput = {
fileName?: string;
fileSizeMB?: string;
requestKey?: string;
autoStart?: boolean;
};
export const buildSQLFileExecutionWorkbenchTab = (
@@ -33,6 +34,10 @@ export const buildSQLFileExecutionWorkbenchTab = (
const fileName = String(input.fileName || '').trim();
const defaultTitle = fileName || t('sidebar.sql_file_exec.title');
const requestKey = input.autoStart === false
? ''
: String(input.requestKey || `sql-file-execution-${Date.now()}`).trim();
return {
id: resolveSQLFileExecutionWorkbenchTabId(connectionId, dbName || undefined, filePath),
title: defaultTitle,
@@ -41,6 +46,6 @@ export const buildSQLFileExecutionWorkbenchTab = (
...(dbName ? { dbName } : {}),
filePath,
sqlFileExecutionFileSizeMB: String(input.fileSizeMB || '').trim() || undefined,
sqlFileExecutionRequestKey: String(input.requestKey || `sql-file-execution-${Date.now()}`).trim(),
...(requestKey ? { sqlFileExecutionRequestKey: requestKey } : {}),
};
};

View File

@@ -478,6 +478,7 @@ export const getTabDisplayKindLabel = (tab: TabData): string => {
if (tab.type === 'design') return 'DESIGN';
if (tab.type === 'table-overview') return 'DB';
if (tab.type === 'table-export') return 'EXPORT';
if (tab.type === 'data-sync') return 'SYNC';
if (tab.type === 'sql-analysis') return 'ANALYZE';
if (tab.type === 'sql-audit') return 'AUDIT';
if (tab.type.startsWith('redis')) return 'REDIS';

View File

@@ -3,7 +3,9 @@ import { afterEach, describe, expect, it } from 'vitest';
import {
buildBatchDatabaseExportWorkbenchTab,
buildBatchTableExportWorkbenchTab,
buildDatabaseExportWorkbenchTab,
buildExportWorkbenchHistoryKey,
buildSchemaExportWorkbenchTab,
buildTableExportHistoryKey,
buildTableExportTab,
DEFAULT_TABLE_EXPORT_SCOPE_OPTION,
@@ -31,6 +33,17 @@ describe('tableExportTab', () => {
dbName: ' ignored ',
exportWorkbenchMode: 'batch-databases',
})).toBe('conn-1::__batch_databases__');
expect(buildExportWorkbenchHistoryKey({
connectionId: ' conn-1 ',
dbName: ' app ',
exportWorkbenchMode: 'database',
})).toBe('conn-1::app::__database__');
expect(buildExportWorkbenchHistoryKey({
connectionId: ' conn-1 ',
dbName: ' app ',
schemaName: ' sales ',
exportWorkbenchMode: 'schema',
})).toBe('conn-1::app::sales::__schema__');
});
it('builds a stable table export tab with normalized defaults', () => {
@@ -99,6 +112,24 @@ describe('tableExportTab', () => {
expect(tab.dbName).toBe('SYS');
});
it('carries selected objects and an auto-start request into the batch table workbench', () => {
const tab = buildBatchTableExportWorkbenchTab({
connectionId: 'conn-1',
dbName: 'SYS',
initialObjectNames: [' users ', 'orders', 'users'],
contentMode: 'backup',
includeDropIfExists: true,
requestKey: 'batch-tables-1',
});
expect(tab).toEqual(expect.objectContaining({
tableExportInitialObjectNames: ['users', 'orders'],
tableExportContentMode: 'backup',
tableExportIncludeDropIfExists: true,
tableExportRequestKey: 'batch-tables-1',
}));
});
it('builds batch database export workbench tabs with stable ids', () => {
setCurrentLanguage('zh-CN');
const tab = buildBatchDatabaseExportWorkbenchTab({
@@ -111,6 +142,57 @@ describe('tableExportTab', () => {
expect(tab.exportWorkbenchMode).toBe('batch-databases');
});
it('carries selected databases and an auto-start request into the batch database workbench', () => {
const tab = buildBatchDatabaseExportWorkbenchTab({
connectionId: 'conn-1',
initialDatabaseNames: [' app ', 'audit', 'app'],
contentMode: 'schema',
includeDropIfExists: true,
requestKey: 'batch-databases-1',
});
expect(tab).toEqual(expect.objectContaining({
tableExportInitialDatabaseNames: ['app', 'audit'],
tableExportContentMode: 'schema',
tableExportIncludeDropIfExists: true,
tableExportRequestKey: 'batch-databases-1',
}));
});
it('builds direct database and schema workbenches with stable targets', () => {
const databaseTab = buildDatabaseExportWorkbenchTab({
connectionId: 'conn-1',
dbName: ' app ',
contentMode: 'backup',
requestKey: 'database-1',
});
const schemaTab = buildSchemaExportWorkbenchTab({
connectionId: 'conn-1',
dbName: 'app',
schemaName: ' sales ',
contentMode: 'schema',
requestKey: 'schema-1',
});
expect(databaseTab).toEqual(expect.objectContaining({
id: 'table-export-database-conn-1-app',
type: 'table-export',
exportWorkbenchMode: 'database',
dbName: 'app',
tableExportContentMode: 'backup',
tableExportRequestKey: 'database-1',
}));
expect(schemaTab).toEqual(expect.objectContaining({
id: 'table-export-schema-conn-1-app-sales',
type: 'table-export',
exportWorkbenchMode: 'schema',
dbName: 'app',
schemaName: 'sales',
tableExportContentMode: 'schema',
tableExportRequestKey: 'schema-1',
}));
});
it('uses the current language for batch workbench fallback titles', () => {
setCurrentLanguage('en-US');

View File

@@ -1,4 +1,4 @@
import type { TabData, TableExportScope, TableExportScopeOption } from '../types';
import type { TabData, TableExportContentMode, TableExportScope, TableExportScopeOption } from '../types';
import { t } from '../i18n';
export const DEFAULT_TABLE_EXPORT_SCOPE_OPTION: TableExportScopeOption = {
@@ -24,7 +24,7 @@ export const buildTableExportHistoryKey = (
};
export const buildExportWorkbenchHistoryKey = (
input: Pick<TabData, 'connectionId' | 'dbName' | 'tableName' | 'exportWorkbenchMode'>,
input: Pick<TabData, 'connectionId' | 'dbName' | 'tableName' | 'schemaName' | 'exportWorkbenchMode'>,
): string => {
const mode = input.exportWorkbenchMode || 'single';
const connectionId = String(input.connectionId || '').trim();
@@ -35,9 +35,21 @@ export const buildExportWorkbenchHistoryKey = (
if (mode === 'batch-databases') {
return [connectionId, '__batch_databases__'].join('::');
}
if (mode === 'database') {
return [connectionId, dbName, '__database__'].join('::');
}
if (mode === 'schema') {
return [connectionId, dbName, String(input.schemaName || '').trim(), '__schema__'].join('::');
}
return buildTableExportHistoryKey(connectionId, dbName, input.tableName);
};
type ExportWorkbenchLaunchOptions = {
contentMode?: TableExportContentMode;
includeDropIfExists?: boolean;
requestKey?: string;
};
type BuildTableExportTabInput = {
connectionId: string;
dbName?: string;
@@ -52,17 +64,51 @@ type BuildTableExportTabInput = {
rowCountByScope?: Partial<Record<TableExportScope, number>>;
};
type BuildBatchTableExportWorkbenchTabInput = {
type BuildBatchTableExportWorkbenchTabInput = ExportWorkbenchLaunchOptions & {
connectionId: string;
dbName?: string;
title?: string;
initialObjectNames?: string[];
};
type BuildBatchDatabaseExportWorkbenchTabInput = {
type BuildBatchDatabaseExportWorkbenchTabInput = ExportWorkbenchLaunchOptions & {
connectionId: string;
title?: string;
initialDatabaseNames?: string[];
};
type BuildDatabaseExportWorkbenchTabInput = ExportWorkbenchLaunchOptions & {
connectionId: string;
dbName: string;
title?: string;
};
type BuildSchemaExportWorkbenchTabInput = ExportWorkbenchLaunchOptions & {
connectionId: string;
dbName: string;
schemaName: string;
title?: string;
};
const normalizeNameList = (values: string[] | undefined): string[] | undefined => {
if (!Array.isArray(values)) return undefined;
const seen = new Set<string>();
const result = values
.map((value) => String(value || '').trim())
.filter((value) => {
if (!value || seen.has(value)) return false;
seen.add(value);
return true;
});
return result.length > 0 ? result : undefined;
};
const buildLaunchMetadata = (input: ExportWorkbenchLaunchOptions): Partial<TabData> => ({
...(input.contentMode ? { tableExportContentMode: input.contentMode } : {}),
...(input.includeDropIfExists === true ? { tableExportIncludeDropIfExists: true } : {}),
...(String(input.requestKey || '').trim() ? { tableExportRequestKey: String(input.requestKey).trim() } : {}),
});
const normalizeScopeOptions = (
scopeOptions: TableExportScopeOption[] | undefined,
): TableExportScopeOption[] => {
@@ -169,6 +215,10 @@ export const buildBatchTableExportWorkbenchTab = (
connectionId,
dbName: dbName || undefined,
initialTab: 'config',
...(normalizeNameList(input.initialObjectNames)
? { tableExportInitialObjectNames: normalizeNameList(input.initialObjectNames) }
: {}),
...buildLaunchMetadata(input),
};
};
@@ -183,5 +233,49 @@ export const buildBatchDatabaseExportWorkbenchTab = (
exportWorkbenchMode: 'batch-databases',
connectionId,
initialTab: 'config',
...(normalizeNameList(input.initialDatabaseNames)
? { tableExportInitialDatabaseNames: normalizeNameList(input.initialDatabaseNames) }
: {}),
...buildLaunchMetadata(input),
};
};
export const buildDatabaseExportWorkbenchTab = (
input: BuildDatabaseExportWorkbenchTabInput,
): TabData => {
const connectionId = String(input.connectionId || '').trim();
const dbName = String(input.dbName || '').trim();
const targetName = dbName || t('data_export.progress.value.target_fallback');
return {
id: `table-export-database-${connectionId || 'none'}-${dbName || 'default'}`,
title: String(input.title || t('data_export.workbench.task.export_target', { name: targetName })).trim()
|| t('data_export.workbench.task.export_target', { name: targetName }),
type: 'table-export',
exportWorkbenchMode: 'database',
connectionId,
dbName: dbName || undefined,
initialTab: 'config',
...buildLaunchMetadata(input),
};
};
export const buildSchemaExportWorkbenchTab = (
input: BuildSchemaExportWorkbenchTabInput,
): TabData => {
const connectionId = String(input.connectionId || '').trim();
const dbName = String(input.dbName || '').trim();
const schemaName = String(input.schemaName || '').trim();
const targetName = [dbName, schemaName].filter(Boolean).join('.') || t('data_export.progress.value.target_fallback');
return {
id: `table-export-schema-${connectionId || 'none'}-${dbName || 'default'}-${schemaName || 'schema'}`,
title: String(input.title || t('data_export.workbench.task.export_target', { name: targetName })).trim()
|| t('data_export.workbench.task.export_target', { name: targetName }),
type: 'table-export',
exportWorkbenchMode: 'schema',
connectionId,
dbName: dbName || undefined,
schemaName: schemaName || undefined,
initialTab: 'config',
...buildLaunchMetadata(input),
};
};