mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-27 08:48:45 +08:00
✨ feat(query-editor): 收敛 SQL 分析工作台与结果区日志体验
- 新增 SQL 分析工作台,统一承载慢 SQL 和 SQL 诊断视图 - 将 SQL 执行日志收进结果区首个日志标签并在失败时展示错误摘要 - 调整侧边栏入口、标签展示、多语言文案与定向前端测试覆盖
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user