diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d45a75d..5b72807 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import ConnectionModal from './components/ConnectionModal'; import SnippetSettingsModal from './components/SnippetSettingsModal'; import ConnectionPackagePasswordModal from './components/ConnectionPackagePasswordModal'; import DataSyncModal from './components/DataSyncModal'; +import { type DataSyncEntryMode } from './components/dataSyncEntryMode'; import DriverManagerModal from './components/DriverManagerModal'; import LinuxCJKFontBanner from './components/LinuxCJKFontBanner'; import LogPanel from './components/LogPanel'; @@ -208,6 +209,7 @@ function App() { const [isModalOpen, setIsModalOpen] = useState(false); const [isConnectionModalMounted, setIsConnectionModalMounted] = useState(false); const [isSyncModalOpen, setIsSyncModalOpen] = useState(false); + const [syncModalEntryMode, setSyncModalEntryMode] = useState('sync'); const [isDriverModalOpen, setIsDriverModalOpen] = useState(false); const [editingConnection, setEditingConnection] = useState(null); const connectionModalWarmupDoneRef = useRef(false); @@ -3759,13 +3761,36 @@ function App() { void handleExportConnections(); }, }, + { + key: 'schema-compare', + icon: , + title: '表结构比对', + description: '对比源表与目标表结构差异,只预览不执行。', + onClick: () => { + setIsToolsModalOpen(false); + setSyncModalEntryMode('schemaCompare'); + setIsSyncModalOpen(true); + }, + }, + { + key: 'data-compare', + icon: , + title: '数据比对', + description: '按主键分析新增、更新、删除和相同行。', + onClick: () => { + setIsToolsModalOpen(false); + setSyncModalEntryMode('dataCompare'); + setIsSyncModalOpen(true); + }, + }, { key: 'sync', icon: , title: '数据同步', - description: '进入跨源同步工作流。', + description: '进入可执行写入的跨源同步工作流。', onClick: () => { setIsToolsModalOpen(false); + setSyncModalEntryMode('sync'); setIsSyncModalOpen(true); }, }, @@ -3976,6 +4001,7 @@ function App() { setIsSyncModalOpen(false)} + entryMode={syncModalEntryMode} /> )} {isDriverModalOpen && ( diff --git a/frontend/src/components/ConnectionModal.edit-password.test.tsx b/frontend/src/components/ConnectionModal.edit-password.test.tsx index 112d093..8325bc6 100644 --- a/frontend/src/components/ConnectionModal.edit-password.test.tsx +++ b/frontend/src/components/ConnectionModal.edit-password.test.tsx @@ -163,6 +163,16 @@ describe('ConnectionModal data source registry', () => { expect(source).toContain('type === "goldendb" ? "goldendb" : "mysql"'); expect(source).toContain('? "goldendb"'); }); + + it('keeps OceanBase Oracle service name optional for OBClient/MySQL-wire connections', () => { + expect(source).toContain('OceanBase Oracle 服务名 (Service Name,可选)'); + expect(source).toContain('isOceanBaseOracle\n ? []'); + expect(source).toContain('连接 OBClient/OBServer MySQL-wire 入口时可留空'); + expect(source).toContain('只有连接 OBProxy Oracle listener/TNS 入口时才需要填写 SERVICE_NAME'); + expect(source).toContain('createUriAwareRequiredRule("请输入 Oracle 服务名(例如 ORCLPDB1)")'); + expect(source).not.toContain('请输入 OceanBase Oracle 服务名'); + expect(source).not.toContain('Oracle 租户必须填写监听器注册的 SERVICE_NAME'); + }); }); describe('ConnectionModal Redis Sentinel configuration', () => { diff --git a/frontend/src/components/ConnectionModal.tsx b/frontend/src/components/ConnectionModal.tsx index ee4cb48..c5b40ee 100644 --- a/frontend/src/components/ConnectionModal.tsx +++ b/frontend/src/components/ConnectionModal.tsx @@ -2075,7 +2075,7 @@ const ConnectionModal: React.FC<{ const scheme = dbType === "diros" ? "doris" : dbType === "starrocks" ? "starrocks" : dbType === "oceanbase" ? "oceanbase" : dbType === "goldendb" ? "goldendb" : "mysql"; if (dbType === "oceanbase") { - return `${scheme}://sys%40oracle001:pass@127.0.0.1:${defaultPort}/SERVICE_NAME?protocol=oracle`; + return `${scheme}://sys%40oracle001:pass@127.0.0.1:${defaultPort}?protocol=oracle`; } return `${scheme}://user:pass@127.0.0.1:${defaultPort},127.0.0.2:${defaultPort}/db_name?topology=replica`; } @@ -4227,14 +4227,6 @@ const ConnectionModal: React.FC<{ ? currentDriverSnapshot.message || `${currentDriverSnapshot.name || dbType} 驱动未安装启用` : ""; - const currentDriverUpdateReason = - hasCurrentDriverType && - currentDriverSnapshot?.connectable && - currentDriverSnapshot.needsUpdate - ? currentDriverSnapshot.message || - currentDriverSnapshot.updateReason || - `${currentDriverSnapshot.name || dbType} 驱动代理需要重装后才能应用当前版本的驱动侧更新` - : ""; const driverStatusChecking = hasCurrentDriverType && !driverStatusLoaded && step === 2; @@ -5246,9 +5238,9 @@ const ConnectionModal: React.FC<{ label="OceanBase 协议" help={ - MySQL 租户选择 MySQL;Oracle 租户选择 Oracle。GoNavi 会根据端口自动选择:OB MySQL wire 端口走 OBClient capability 注入(与 Navicat 相同路径),OBProxy Oracle listener 端口走标准 TNS。 + MySQL 租户选择 MySQL;Oracle 租户选择 Oracle。Oracle 协议会优先使用 OBClient/OBServer MySQL-wire 入口;只有连接 OBProxy Oracle listener/TNS 入口时才需要 Service Name。
- 如果 Oracle 租户连接报「Error 1235」或 OBClient 握手失败,可在「连接参数」字段通过 connectionAttributes=key1:value1,key2:value2 覆盖 GoNavi 默认注入的 OBClient capability。 + 如果 MySQL 协议连接 Oracle 租户报「Error 1235」,请切换为 Oracle 协议;通常不需要手工配置 connectionAttributes。
} style={{ marginBottom: 0 }} @@ -5356,24 +5348,22 @@ const ConnectionModal: React.FC<{ children: ( ), @@ -6778,26 +6768,6 @@ const ConnectionModal: React.FC<{ } /> )} - {currentDriverUpdateReason && ( - - {currentDriverUpdateReason} - - - } - /> - )} {(() => { const sectionItems: Array<{ key: "basic" | "network" | "appearance"; diff --git a/frontend/src/components/DataSyncModal.entry-mode.test.ts b/frontend/src/components/DataSyncModal.entry-mode.test.ts new file mode 100644 index 0000000..d26c84a --- /dev/null +++ b/frontend/src/components/DataSyncModal.entry-mode.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveDataSyncEntryModePresentation } from './dataSyncEntryMode'; + +describe('resolveDataSyncEntryModePresentation', () => { + it('marks schema compare as a read-only independent entry', () => { + const presentation = resolveDataSyncEntryModePresentation('schemaCompare'); + + expect(presentation.title).toBe('表结构比对'); + expect(presentation.analyzeButtonText).toBe('开始比对'); + expect(presentation.badgeText).toBe('结构比对'); + expect(presentation.readOnly).toBe(true); + }); + + it('marks data compare as a read-only independent entry', () => { + const presentation = resolveDataSyncEntryModePresentation('dataCompare'); + + expect(presentation.title).toBe('数据比对'); + expect(presentation.tableSelectLabel).toContain('比对数据'); + expect(presentation.badgeText).toBe('数据比对'); + expect(presentation.readOnly).toBe(true); + }); + + it('keeps the original sync entry writable', () => { + const presentation = resolveDataSyncEntryModePresentation('sync'); + + expect(presentation.title).toBe('数据同步工作台'); + expect(presentation.analyzeButtonText).toBe('对比差异'); + expect(presentation.badgeText).toBe('同步模式'); + expect(presentation.readOnly).toBe(false); + }); +}); diff --git a/frontend/src/components/DataSyncModal.tsx b/frontend/src/components/DataSyncModal.tsx index 7efd45b..1c65490 100644 --- a/frontend/src/components/DataSyncModal.tsx +++ b/frontend/src/components/DataSyncModal.tsx @@ -10,6 +10,7 @@ import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; import { quoteIdentPart, quoteQualifiedIdent } from '../utils/sql'; import { formatLocalDateTimeLiteral, normalizeTemporalLiteralText } from './dataGridCopyInsert'; import { buildDataSyncRequest, type SourceDatasetMode, validateDataSyncSelection } from './dataSyncRequest'; +import { resolveDataSyncEntryModePresentation, type DataSyncEntryMode } from './dataSyncEntryMode'; const { Title, Text } = Typography; const { Step } = Steps; const { Option } = Select; @@ -188,7 +189,11 @@ const buildSqlPreview = ( }; }; -const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, onClose }) => { +const DataSyncModal: React.FC<{ open: boolean; onClose: () => void; entryMode?: DataSyncEntryMode }> = ({ open, onClose, entryMode = 'sync' }) => { + const entryPresentation = resolveDataSyncEntryModePresentation(entryMode); + const isSchemaCompareEntry = entryMode === 'schemaCompare'; + const isDataCompareEntry = entryMode === 'dataCompare'; + const isCompareEntry = entryPresentation.readOnly; const connections = useStore((state) => state.connections); const themeMode = useStore((state) => state.theme); const appearance = useStore((state) => state.appearance); @@ -217,7 +222,7 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, // Options const [workflowType, setWorkflowType] = useState('sync'); - const [syncContent, setSyncContent] = useState<'data' | 'schema' | 'both'>('data'); + const [syncContent, setSyncContent] = useState<'data' | 'schema' | 'both'>(isSchemaCompareEntry ? 'schema' : 'data'); const [syncMode, setSyncMode] = useState('insert_update'); const [autoAddColumns, setAutoAddColumns] = useState(true); const [targetTableStrategy, setTargetTableStrategy] = useState<'existing_only' | 'auto_create_if_missing' | 'smart'>('existing_only'); @@ -276,8 +281,8 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, }); return () => { - offLog(); - offProgress(); + if (typeof offLog === 'function') offLog(); + if (typeof offProgress === 'function') offProgress(); }; }, [open]); @@ -299,7 +304,7 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, setSourceDatasetMode('table'); setSourceQuery(''); setWorkflowType('sync'); - setSyncContent('data'); + setSyncContent(isSchemaCompareEntry ? 'schema' : 'data'); setSyncMode('insert_update'); setAutoAddColumns(true); setTargetTableStrategy('existing_only'); @@ -319,9 +324,48 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, jobIdRef.current = ''; autoScrollRef.current = true; } - }, [open]); + }, [open, isSchemaCompareEntry]); useEffect(() => { + if (isSchemaCompareEntry) { + if (workflowType !== 'sync') { + setWorkflowType('sync'); + } + if (sourceDatasetMode !== 'table') { + setSourceDatasetMode('table'); + } + if (syncContent !== 'schema') { + setSyncContent('schema'); + } + if (syncMode !== 'insert_update') { + setSyncMode('insert_update'); + } + if (targetTableStrategy !== 'existing_only') { + setTargetTableStrategy('existing_only'); + } + if (createIndexes) { + setCreateIndexes(false); + } + return; + } + if (isDataCompareEntry) { + if (workflowType !== 'sync') { + setWorkflowType('sync'); + } + if (syncContent !== 'data') { + setSyncContent('data'); + } + if (syncMode !== 'insert_update') { + setSyncMode('insert_update'); + } + if (targetTableStrategy !== 'existing_only') { + setTargetTableStrategy('existing_only'); + } + if (createIndexes) { + setCreateIndexes(false); + } + return; + } if (workflowType === 'migration') { if (syncMode === 'insert_update') { setSyncMode('insert_only'); @@ -343,7 +387,7 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, setCreateIndexes(false); } } - }, [workflowType]); + }, [isSchemaCompareEntry, isDataCompareEntry, workflowType, sourceDatasetMode, syncContent, syncMode, targetTableStrategy, createIndexes]); useEffect(() => { if (sourceDatasetMode !== 'query') return; @@ -371,38 +415,38 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, setSourceConnId(connId); setSourceDb(''); const conn = connections.find(c => c.id === connId); - if (conn) { - setLoading(true); - try { - const res = await DBGetDatabases(normalizeConnConfig(conn) as any); - if (res.success) { - const dbRows = Array.isArray(res.data) ? res.data : []; - setSourceDbs(dbRows - .map((r: any) => r?.Database || r?.database || r?.username) - .filter((name: any) => typeof name === 'string' && name.trim() !== '')); - } - } catch(e) { message.error("Failed to fetch source databases"); } - setLoading(false); - } + if (conn) { + setLoading(true); + try { + const res = await DBGetDatabases(normalizeConnConfig(conn) as any); + if (res.success) { + const dbRows = Array.isArray(res.data) ? res.data : []; + setSourceDbs(dbRows + .map((r: any) => r?.Database || r?.database || r?.username) + .filter((name: any) => typeof name === 'string' && name.trim() !== '')); + } + } catch(e) { message.error("Failed to fetch source databases"); } + setLoading(false); + } }; const handleTargetConnChange = async (connId: string) => { setTargetConnId(connId); setTargetDb(''); const conn = connections.find(c => c.id === connId); - if (conn) { - setLoading(true); - try { - const res = await DBGetDatabases(normalizeConnConfig(conn) as any); - if (res.success) { - const dbRows = Array.isArray(res.data) ? res.data : []; - setTargetDbs(dbRows - .map((r: any) => r?.Database || r?.database || r?.username) - .filter((name: any) => typeof name === 'string' && name.trim() !== '')); - } - } catch(e) { message.error("Failed to fetch target databases"); } - setLoading(false); - } + if (conn) { + setLoading(true); + try { + const res = await DBGetDatabases(normalizeConnConfig(conn) as any); + if (res.success) { + const dbRows = Array.isArray(res.data) ? res.data : []; + setTargetDbs(dbRows + .map((r: any) => r?.Database || r?.database || r?.username) + .filter((name: any) => typeof name === 'string' && name.trim() !== '')); + } + } catch(e) { message.error("Failed to fetch target databases"); } + setLoading(false); + } }; const nextToTables = async () => { @@ -416,15 +460,15 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, const dbName = isSourceQueryMode ? targetDb : sourceDb; const conn = connections.find(c => c.id === connId); if (conn) { - const config = normalizeConnConfig(conn, dbName); - const res = await DBGetTables(config as any, dbName); - if (res.success) { - // DBGetTables returns [{Table: "name"}, ...] - const tableRows = Array.isArray(res.data) ? res.data : []; - const tables = tableRows - .map((row: any) => row?.Table || row?.table || row?.TABLE_NAME || Object.values(row || {})[0]) - .filter((name: any) => typeof name === 'string' && name.trim() !== ''); - setAllTables(tables as string[]); + const config = normalizeConnConfig(conn, dbName); + const res = await DBGetTables(config as any, dbName); + if (res.success) { + // DBGetTables returns [{Table: "name"}, ...] + const tableRows = Array.isArray(res.data) ? res.data : []; + const tables = tableRows + .map((row: any) => row?.Table || row?.table || row?.TABLE_NAME || Object.values(row || {})[0]) + .filter((name: any) => typeof name === 'string' && name.trim() !== ''); + setAllTables(tables as string[]); setSelectedTables(prev => { const existing = prev.filter((name) => tables.includes(name)); if (isSourceQueryMode) { @@ -432,8 +476,8 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, } return existing; }); - setCurrentStep(1); - } else { + setCurrentStep(1); + } else { message.error(res.message); } } @@ -681,7 +725,7 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, }, [diffTables]); const isSourceQueryMode = sourceDatasetMode === 'query'; - const isMigrationWorkflow = workflowType === 'migration'; + const isMigrationWorkflow = !isCompareEntry && workflowType === 'migration'; const sourceConn = useMemo(() => connections.find(c => c.id === sourceConnId), [connections, sourceConnId]); const targetConn = useMemo(() => connections.find(c => c.id === targetConnId), [connections, targetConnId]); const sourceType = String(sourceConn?.config?.type || '').toLowerCase(); @@ -797,7 +841,7 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, return ( <> { if (syncing) { @@ -830,15 +874,15 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open,
-
{isMigrationWorkflow ? '跨数据源迁移' : '数据同步'}
+
{isMigrationWorkflow ? '跨数据源迁移' : entryPresentation.heroTitle}
{isMigrationWorkflow ? '适合把源表迁移到另一套数据库,可按策略自动建表、导入数据并补建可兼容索引。' - : '适合目标表已存在的场景,先做差异分析,再按勾选执行插入、更新或删除。'} + : entryPresentation.heroDescription}
- {isMigrationWorkflow ? : } {isMigrationWorkflow ? '迁移模式' : '同步模式'} + {isMigrationWorkflow ? : } {isMigrationWorkflow ? '迁移模式' : entryPresentation.badgeText} {sourceConnId ? '已选源连接' : '待选源连接'} {selectedTables.length || 0} 张表
@@ -847,7 +891,7 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, - +
@@ -900,35 +944,45 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open,
- 先明确当前要做的是“已有目标表同步”还是“跨库迁移”,页面会按功能类型自动给出更安全的默认策略。 + {isCompareEntry + ? '当前入口只做差异分析和预览,不会执行同步、建表、补字段或删除数据。' + : '先明确当前要做的是“已有目标表同步”还是“跨库迁移”,页面会按功能类型自动给出更安全的默认策略。'}
- - - - - - + {!isCompareEntry && ( + + + + )} + {!isSchemaCompareEntry && ( + + + + )} {isSourceQueryMode && ( void }> = ({ open, message="SQL 结果集同步当前只支持:源端自定义 SQL -> 单个已存在目标表;查询结果需包含目标表主键列。" /> )} - - - - - - - - - + {!isCompareEntry && ( + + + + )} + {!isCompareEntry && ( + + + + )} + {!isCompareEntry && ( + + + + )} {isRedisMongoKeyspaceMigration && ( void }> = ({ open, /> )} - - setAutoAddColumns(e.target.checked)} disabled={isSourceQueryMode}> - 自动补齐目标表缺失字段(按源/目标数据源选择可兼容规划器;SQL 结果集模式暂不支持) - - - - setCreateIndexes(e.target.checked)} disabled={!isMigrationWorkflow || targetTableStrategy === 'existing_only' || isSourceQueryMode}> - 自动迁移可兼容的普通索引/唯一索引(仅自动建表模式生效) - - + {(!isCompareEntry || isSchemaCompareEntry) && ( + + setAutoAddColumns(e.target.checked)} disabled={isSourceQueryMode}> + {isSchemaCompareEntry + ? '生成目标表缺失字段的兼容变更 SQL(仅预览,不执行)' + : '自动补齐目标表缺失字段(按源/目标数据源选择可兼容规划器;SQL 结果集模式暂不支持)'} + + + )} + {!isCompareEntry && ( + + setCreateIndexes(e.target.checked)} disabled={!isMigrationWorkflow || targetTableStrategy === 'existing_only' || isSourceQueryMode}> + 自动迁移可兼容的普通索引/唯一索引(仅自动建表模式生效) + + + )} {isMigrationWorkflow && targetTableStrategy !== 'existing_only' && ( void }> = ({ open, style={{ marginBottom: 12 }} /> )} - {!isMigrationWorkflow && ( + {!isCompareEntry && !isMigrationWorkflow && ( void }> = ({ open, {!isSourceQueryMode && ( <>
- 请选择需要同步的表: + {entryPresentation.tableSelectLabel} setShowSameTables(e.target.checked)}> 显示相同表 @@ -1204,48 +1270,50 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, {currentStep === 2 && (
- - -
- `${syncProgress.current}/${syncProgress.total}`} + -
+ +
+ `${syncProgress.current}/${syncProgress.total}`} + /> +
- 执行日志 -
{ - const el = logBoxRef.current; - if (!el) return; - const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40; - autoScrollRef.current = nearBottom; - }} - style={{ - background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(248,250,252,0.92)', - border: darkMode ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(15,23,42,0.06)', - borderRadius: 14, - padding: 12, - height: 300, - overflowY: 'auto', - fontFamily: 'var(--gn-font-mono)' - }} - > - {syncLogs.map((item, i: number) =>
{renderSyncLogItem(item)}
)} -
+ {isCompareEntry ? '分析日志' : '执行日志'} +
{ + const el = logBoxRef.current; + if (!el) return; + const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40; + autoScrollRef.current = nearBottom; + }} + style={{ + background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(248,250,252,0.92)', + border: darkMode ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(15,23,42,0.06)', + borderRadius: 14, + padding: 12, + height: 300, + overflowY: 'auto', + fontFamily: 'var(--gn-font-mono)' + }} + > + {syncLogs.map((item, i: number) =>
{renderSyncLogItem(item)}
)} +
)} @@ -1256,25 +1324,32 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, {currentStep === 0 && ( )} - {currentStep === 1 && ( - <> - - - + {currentStep === 1 && ( + <> + + + {isCompareEntry && ( + + )} + {!isCompareEntry && ( + + )} )} {currentStep === 2 && ( <> - + )} @@ -1351,7 +1426,7 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, label: `插入(${previewData.totalInserts || 0})`, children: (
- 未勾选任何行表示“同步全部插入差异”;如不想执行插入请在对比结果中取消勾选“插入”。 + {isCompareEntry ? '行选择只影响 SQL 预览范围,不会执行写入。' : '未勾选任何行表示“同步全部插入差异”;如不想执行插入请在对比结果中取消勾选“插入”。'} void }> = ({ open, label: `更新(${previewData.totalUpdates || 0})`, children: (
- 未勾选任何行表示“同步全部更新差异”;如不想执行更新请在对比结果中取消勾选“更新”。 + {isCompareEntry ? '行选择只影响 SQL 预览范围,不会执行写入。' : '未勾选任何行表示“同步全部更新差异”;如不想执行更新请在对比结果中取消勾选“更新”。'}
void }> = ({ open, children: (
- 未勾选任何行表示“同步全部删除差异”;如不想执行删除请在对比结果中取消勾选“删除”。 + {isCompareEntry ? '行选择只影响 SQL 预览范围,不会执行写入。' : '未勾选任何行表示“同步全部删除差异”;如不想执行删除请在对比结果中取消勾选“删除”。'}
void }> = ({ open, showIcon message={ previewHasDataDiff - ? "SQL 预览会按当前勾选的插入/更新/删除与行选择范围生成,用于审核确认。" - : "SQL 预览展示将执行的结构变更语句,用于审核确认。" + ? (isCompareEntry ? 'SQL 预览会按当前勾选的插入/更新/删除与行选择范围生成,仅用于审核差异。' : 'SQL 预览会按当前勾选的插入/更新/删除与行选择范围生成,用于审核确认。') + : (isCompareEntry ? 'SQL 预览展示结构差异建议语句,仅用于审核差异。' : 'SQL 预览展示将执行的结构变更语句,用于审核确认。') } />
diff --git a/frontend/src/components/DriverManagerModal.test.tsx b/frontend/src/components/DriverManagerModal.test.tsx index 449d52c..affbb65 100644 --- a/frontend/src/components/DriverManagerModal.test.tsx +++ b/frontend/src/components/DriverManagerModal.test.tsx @@ -1,9 +1,14 @@ import React from 'react'; +import { readFileSync } from 'node:fs'; import { act, create, type ReactTestRenderer } from 'react-test-renderer'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import DriverManagerModal from './DriverManagerModal'; +const connectionModalSource = readFileSync(new URL('./ConnectionModal.tsx', import.meta.url), 'utf8'); +const driverManagerModalSource = readFileSync(new URL('./DriverManagerModal.tsx', import.meta.url), 'utf8'); +const sidebarSource = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); + const storeState = vi.hoisted(() => ({ theme: 'light', appearance: { @@ -57,6 +62,20 @@ vi.mock('@ant-design/icons', () => { }; }); +describe('driver-agent update prompt placement', () => { + it('keeps revision mismatch prompts inside driver manager only', () => { + expect(driverManagerModalSource).toContain('需要重装'); + expect(driverManagerModalSource).toContain('row.needsUpdate'); + + expect(connectionModalSource).not.toContain('当前数据源驱动代理建议重装'); + expect(connectionModalSource).not.toContain('去驱动管理重装'); + + expect(sidebarSource).not.toContain('warnIfConnectionDriverAgentNeedsUpdate'); + expect(sidebarSource).not.toContain('driver-agent-update-'); + expect(sidebarSource).not.toContain('驱动代理需要重装:'); + }); +}); + vi.mock('antd', () => { const Button: any = ({ children, disabled, loading, onClick, ...rest }: any) => (