feat(query-editor): 收敛 SQL 分析工作台与结果区日志体验

- 新增 SQL 分析工作台,统一承载慢 SQL 和 SQL 诊断视图
- 将 SQL 执行日志收进结果区首个日志标签并在失败时展示错误摘要
- 调整侧边栏入口、标签展示、多语言文案与定向前端测试覆盖
This commit is contained in:
Syngnat
2026-06-20 14:09:58 +08:00
parent 04019135a0
commit c8c8497a2f
26 changed files with 1539 additions and 539 deletions

View File

@@ -1,5 +1,5 @@
import Modal from './common/ResizableDraggableModal';
import React, { useState, useEffect, useRef, useMemo, useCallback, lazy, Suspense } from 'react';
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import Editor, { type OnMount } from './MonacoEditor';
import { message, Input, Form, MenuProps } from 'antd';
import { format } from 'sql-formatter';
@@ -31,11 +31,8 @@ import { splitSidebarQualifiedName } from '../utils/sidebarLocate';
import { buildMySQLCompatibleViewMetadataSqls, isSidebarViewTableType, normalizeSidebarViewName } from '../utils/sidebarMetadata';
import { SIDEBAR_SQL_EDITOR_DRAG_MIME, decodeSidebarSqlEditorDragPayload, hasSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag';
import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert';
// SQL 诊断工作台lazy 加载避免 reactflow/dagre 进入主 bundle约 130KB gzipped 独立 chunk
const ExplainWorkbench = lazy(() => import('./explain/ExplainWorkbench'));
// 慢 SQL 历史面板lazy 加载
const SlowQueryPanel = lazy(() => import('./explain/SlowQueryPanel'));
import { SUPPORTED_LANGUAGES, t as translate } from '../i18n';
import { buildSqlAnalysisWorkbenchTab } from '../utils/sqlAnalysisTab';
import {
DUCKDB_ROWID_LOCATOR_COLUMN,
ORACLE_ROWID_LOCATOR_COLUMN,
@@ -48,7 +45,10 @@ import {
getColumnDefinitionName,
getColumnDefinitionType,
} from '../utils/columnDefinition';
import QueryEditorResultsPanel, { type QueryEditorResultSet } from './QueryEditorResultsPanel';
import QueryEditorResultsPanel, {
QUERY_EDITOR_SQL_LOG_TAB_KEY,
type QueryEditorResultSet,
} from './QueryEditorResultsPanel';
import { SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS } from './QueryEditorTransactionSettings';
import QueryEditorTransactionToolbar from './QueryEditorTransactionToolbar';
import QueryEditorToolbar from './QueryEditorToolbar';
@@ -230,10 +230,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const [saveModalMode, setSaveModalMode] = useState<'save' | 'rename'>('save');
const [saveForm] = Form.useForm();
// SQL 诊断工作台与慢 SQL 历史:通过快捷键管理系统注册(避免与 toggleTheme/toggleLogPanel 冲突)
const [explainOpen, setExplainOpen] = useState(false);
const [slowQueryOpen, setSlowQueryOpen] = useState(false);
// Database Selection
const [currentConnectionId, setCurrentConnectionId] = useState<string>(tab.connectionId);
const [currentDb, setCurrentDb] = useState<string>(tab.dbName || '');
@@ -278,22 +274,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
[connections]
);
// SQL 诊断工作台:从 currentConnectionId 解析 ConnectionConfig复用 SavedConnection 模式)
const explainConfig = useMemo(() => {
if (!currentConnectionId) return null;
const conn = connections.find(c => c.id === currentConnectionId);
if (!conn) return null;
return {
...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: '' },
} as any;
}, [connections, currentConnectionId]);
const addSqlLog = useStore(state => state.addSqlLog);
const sqlLogCount = useStore(state => state.sqlLogs.length);
const addTab = useStore(state => state.addTab);
const setActiveContext = useStore(state => state.setActiveContext);
const updateQueryTabDraft = useStore(state => state.updateQueryTabDraft);
@@ -334,23 +316,41 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
[activeShortcutPlatform, shortcutOptions],
);
const openSqlAnalysisWorkbench = useCallback(
(view: 'diagnose' | 'slow-query', nextSql?: string) => {
const connectionId = String(currentConnectionId || '').trim();
if (!connectionId) {
message.warning(translate('query_editor.message.connection_not_found'));
return;
}
const dbName = String(currentDb || tab.dbName || '').trim();
addTab(buildSqlAnalysisWorkbenchTab({
connectionId,
dbName: dbName || undefined,
query: typeof nextSql === 'string' && nextSql.trim() ? nextSql : undefined,
view,
}));
},
[addTab, currentConnectionId, currentDb, tab.dbName],
);
// SQL 诊断 / 慢 SQL 历史的快捷键监听(必须在 binding 声明之后)
useEffect(() => {
if (!isActive) return;
const handler = (e: KeyboardEvent) => {
if (diagnoseQueryShortcutBinding?.enabled && isShortcutMatch(e, diagnoseQueryShortcutBinding.combo)) {
e.preventDefault();
setExplainOpen(true);
openSqlAnalysisWorkbench('diagnose', getCurrentQuery());
return;
}
if (showSlowQueriesShortcutBinding?.enabled && isShortcutMatch(e, showSlowQueriesShortcutBinding.combo)) {
e.preventDefault();
setSlowQueryOpen(true);
openSqlAnalysisWorkbench('slow-query');
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [isActive, diagnoseQueryShortcutBinding, showSlowQueriesShortcutBinding]);
}, [diagnoseQueryShortcutBinding, isActive, openSqlAnalysisWorkbench, showSlowQueriesShortcutBinding]);
const selectCurrentStatementShortcutBinding = useMemo(
() => resolveShortcutBinding(shortcutOptions, 'selectCurrentStatement', activeShortcutPlatform),
[activeShortcutPlatform, shortcutOptions],
@@ -381,6 +381,17 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return nextVisible;
});
}, [tab.id, updateQueryTabDraft]);
const handleShowSqlExecutionLog = useCallback(() => {
if (!isActive) {
return;
}
if (isResultPanelVisible && activeResultKey === QUERY_EDITOR_SQL_LOG_TAB_KEY) {
updateResultPanelVisibility(false);
return;
}
updateResultPanelVisibility(true);
setActiveResultKey(QUERY_EDITOR_SQL_LOG_TAB_KEY);
}, [activeResultKey, isActive, isResultPanelVisible, updateResultPanelVisibility]);
const sqlEditorCommitMode = sqlEditorTransactionOptions?.commitMode === 'auto' ? 'auto' : 'manual';
const sqlEditorAutoCommitDelayMs = SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS.some((item) => item.value === sqlEditorTransactionOptions?.autoCommitDelayMs)
? Number(sqlEditorTransactionOptions?.autoCommitDelayMs)
@@ -2960,15 +2971,15 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
dbName: currentDb
});
if (!res.success) {
const prefix = statements.length > 1
? translate('query_editor.message.statement_failed_prefix', { index: idx + 1 })
: '';
updateResultPanelVisibility(true);
setExecutionError(formatSqlExecutionError(res.message, { prefix }));
setResultSets([]);
setActiveResultKey('');
return;
}
const prefix = statements.length > 1
? translate('query_editor.message.statement_failed_prefix', { index: idx + 1 })
: '';
updateResultPanelVisibility(true);
setExecutionError(formatSqlExecutionError(res.message, { prefix }));
setResultSets([]);
setActiveResultKey(QUERY_EDITOR_SQL_LOG_TAB_KEY);
return;
}
if (Array.isArray(res.data)) {
let rows = (res.data as any[]) || [];
let truncated = false;
@@ -3223,7 +3234,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
updateResultPanelVisibility(true);
setExecutionError(formatSqlExecutionError(res.message));
setResultSets([]);
setActiveResultKey('');
setActiveResultKey(QUERY_EDITOR_SQL_LOG_TAB_KEY);
return;
}
@@ -3400,7 +3411,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
updateResultPanelVisibility(true);
setExecutionError(formattedError);
setResultSets([]);
setActiveResultKey('');
setActiveResultKey(QUERY_EDITOR_SQL_LOG_TAB_KEY);
} finally {
if (runSeqRef.current === runSeq) setLoading(false);
// Clear query ID after execution completes
@@ -3881,7 +3892,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
)}
</span>
),
onClick: () => setExplainOpen(true),
onClick: () => openSqlAnalysisWorkbench('diagnose', getCurrentQuery()),
},
{
key: 'show-slow-queries',
@@ -3895,7 +3906,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
)}
</span>
),
onClick: () => setSlowQueryOpen(true),
onClick: () => openSqlAnalysisWorkbench('slow-query'),
},
];
@@ -3979,6 +3990,17 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
}, [isActive, handleQuickSave]);
useEffect(() => {
const handleOpenSqlExecutionLog = () => {
handleShowSqlExecutionLog();
};
window.addEventListener('gonavi:show-sql-execution-log', handleOpenSqlExecutionLog as EventListener);
return () => {
window.removeEventListener('gonavi:show-sql-execution-log', handleOpenSqlExecutionLog as EventListener);
};
}, [handleShowSqlExecutionLog]);
const handleSave = async () => {
try {
const values = await saveForm.validateFields();
@@ -4178,6 +4200,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
activeResultKey={activeResultKey}
loading={loading}
executionError={executionError}
sqlLogCount={sqlLogCount}
darkMode={darkMode}
isV2Ui={isV2Ui}
currentDb={currentDb}
@@ -4211,31 +4234,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
</Form>
</Modal>
{/* SQL 诊断工作台Ctrl+Shift+D 触发lazy 加载避免 reactflow 进入主 bundle */}
<Suspense fallback={null}>
{explainOpen && explainConfig && (
<ExplainWorkbench
open={explainOpen}
onClose={() => setExplainOpen(false)}
config={explainConfig}
dbName={currentDb}
sql={query}
/>
)}
</Suspense>
{/* 慢 SQL 历史Ctrl+Shift+H 触发 */}
<Suspense fallback={null}>
{slowQueryOpen && explainConfig && (
<SlowQueryPanel
open={slowQueryOpen}
onClose={() => setSlowQueryOpen(false)}
config={explainConfig}
dbName={currentDb}
onPickQuery={(sql) => setQuery(sql)}
/>
)}
</Suspense>
</div>
);
};