+ {record.category === 'transaction' && (
+
TX
+ )}
{text}
{record.message &&
{record.message}
}
{record.affectedRows !== undefined &&
{t('log_panel.affected_rows', { count: record.affectedRows })}
}
diff --git a/frontend/src/components/MessagePublishModal.tsx b/frontend/src/components/MessagePublishModal.tsx
index dc324ab0..b4bea4b9 100644
--- a/frontend/src/components/MessagePublishModal.tsx
+++ b/frontend/src/components/MessagePublishModal.tsx
@@ -2,7 +2,7 @@ import Modal from './common/ResizableDraggableModal';
import React, { useEffect, useMemo, useState } from 'react';
import { Alert, Checkbox, Form, Input, Select, Space, Typography, message } from 'antd';
-import { DBQuery } from '../../wailsjs/go/app/App';
+import { DBQueryAudited } from '../../wailsjs/go/app/App';
import type { SavedConnection } from '../types';
import { useI18n } from '../i18n/provider';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
@@ -98,10 +98,11 @@ const MessagePublishModal: React.FC
= ({
setSubmitting(true);
try {
- const res = await DBQuery(
+ const res = await DBQueryAudited(
buildRpcConnectionConfig(connection.config) as any,
executionDbName,
command.commandText,
+ 'message_publish',
);
if (!res?.success) {
void message.error(t('message_publish_modal.error.send_failed_detail', {
diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
index 5751d8fb..07618305 100644
--- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx
+++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
@@ -849,6 +849,7 @@ describe('QueryEditor external SQL save', () => {
backendApp.GenerateQueryID.mockResolvedValue('query-1');
storeState.connections = createDefaultConnections();
storeState.sqlLogs = [];
+ storeState.addSqlLog.mockReset();
storeState.sqlSnippets = [];
storeState.clearSqlLogs.mockReset();
storeState.connections[0].config.type = 'mysql';
@@ -7469,6 +7470,13 @@ describe('QueryEditor external SQL save', () => {
expect(textContent(renderer!.root)).not.toContain('未提交');
expect(textContent(renderer!.root)).toContain('提交');
expect(textContent(renderer!.root)).toContain('影响行数:2');
+ expect(storeState.sqlEditorPendingTransactions['tab-1']).toMatchObject({
+ id: 'tx-1',
+ dbType: 'mysql',
+ dbName: 'main',
+ statements: ["UPDATE users SET name = 'new' WHERE id = 1"],
+ executionDurationMs: expect.any(Number),
+ });
await act(async () => {
await findButton(renderer!, '提交').props.onClick();
@@ -7479,6 +7487,11 @@ describe('QueryEditor external SQL save', () => {
});
expect(backendApp.DBCommitTransaction).toHaveBeenCalledWith('tx-1');
+ expect(storeState.addSqlLog).toHaveBeenCalledWith(expect.objectContaining({
+ sql: "START TRANSACTION;\nUPDATE users SET name = 'new' WHERE id = 1;\nCOMMIT;",
+ status: 'success',
+ dbName: 'main',
+ }));
expect(textContent(renderer!.root)).not.toContain('未提交');
});
@@ -7571,6 +7584,26 @@ describe('QueryEditor external SQL save', () => {
expect(dataGridState.latestProps?.data?.[0]).toMatchObject({ name: 'new' });
expect(textContent(renderer!.root)).toContain('提交');
expect(textContent(renderer!.root)).toContain('回滚');
+ expect(storeState.sqlEditorPendingTransactions['tab-1']).toMatchObject({
+ statements: [
+ "UPDATE users SET name = 'new' WHERE id = 1",
+ 'SELECT name FROM users WHERE id = 1',
+ ],
+ statementCount: 2,
+ });
+
+ await act(async () => {
+ await findButton(renderer!, '提交').props.onClick();
+ });
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(storeState.addSqlLog).toHaveBeenCalledWith(expect.objectContaining({
+ sql: "START TRANSACTION;\nUPDATE users SET name = 'new' WHERE id = 1;\nSELECT name FROM users WHERE id = 1;\nCOMMIT;",
+ status: 'success',
+ }));
});
it('runs SQL editor WITH DML through a pending managed transaction', async () => {
diff --git a/frontend/src/components/QueryEditor.results-and-drop.test.tsx b/frontend/src/components/QueryEditor.results-and-drop.test.tsx
index 2db0a547..5964916d 100644
--- a/frontend/src/components/QueryEditor.results-and-drop.test.tsx
+++ b/frontend/src/components/QueryEditor.results-and-drop.test.tsx
@@ -304,7 +304,7 @@ vi.mock('@monaco-editor/react', () => ({
onMount?.(editorState.editor, {
editor: { setTheme: vi.fn() },
KeyMod: { CtrlCmd: 2048, WinCtrl: 256, Alt: 512, Shift: 1024 },
- KeyCode: { KeyF: 70, KeyM: 77, KeyQ: 81, KeyS: 83 },
+ KeyCode: { KeyF: 70, KeyM: 77, KeyQ: 81, KeyR: 82, KeyS: 83 },
languages: {
CompletionItemKind: { Keyword: 1, Function: 2, Field: 3 },
CompletionItemInsertTextRule: { InsertAsSnippet: 1 },
@@ -1778,6 +1778,66 @@ describe('QueryEditor external SQL save', () => {
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3');
});
+ it('registers Windows Ctrl+R with Monaco CtrlCmd and runs the selected SQL', async () => {
+ storeState.shortcutOptions.runQuery.windows = { enabled: true, combo: 'Ctrl+R' };
+ const windowListeners: Record void)[]> = {};
+ vi.stubGlobal('window', {
+ addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
+ windowListeners[type] ||= [];
+ windowListeners[type].push(listener);
+ }),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn((event: Event) => {
+ windowListeners[event.type]?.forEach((listener) => listener(event));
+ return true;
+ }),
+ requestAnimationFrame: vi.fn((callback: FrameRequestCallback) => {
+ callback(0);
+ return 1;
+ }),
+ cancelAnimationFrame: vi.fn(),
+ innerHeight: 900,
+ });
+ backendApp.DBQueryMulti.mockResolvedValueOnce({
+ success: true,
+ data: [{ columns: ['total'], rows: [{ total: 1 }] }],
+ });
+
+ await act(async () => {
+ create();
+ });
+
+ editorState.selection = {
+ startLineNumber: 2,
+ startColumn: 1,
+ endLineNumber: 2,
+ endColumn: 'select count(*) as total from messages'.length + 1,
+ };
+ const runAction = findEditorAction('gonavi.runQuery');
+ expect(runAction).toMatchObject({
+ keybindings: [2048 | 82],
+ keybindingContext: 'editorTextFocus',
+ });
+
+ await act(async () => {
+ await runAction.run();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(
+ expect.anything(),
+ 'main',
+ expect.stringContaining('select count(*) as total from messages'),
+ 'query-1',
+ );
+ expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 1');
+ expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3');
+ });
+
it('does not run SQL from the run shortcut when nothing is selected', async () => {
storeState.shortcutOptions.runQuery.mac = { enabled: true, combo: 'Meta+Enter' };
storeState.shortcutOptions.runQuery.windows = { enabled: true, combo: 'Ctrl+Enter' };
diff --git a/frontend/src/components/QueryEditor.sql-analysis-workbench.test.ts b/frontend/src/components/QueryEditor.sql-analysis-workbench.test.ts
index 3fb532cf..572a8233 100644
--- a/frontend/src/components/QueryEditor.sql-analysis-workbench.test.ts
+++ b/frontend/src/components/QueryEditor.sql-analysis-workbench.test.ts
@@ -38,6 +38,18 @@ describe('SQL analysis workbench wiring', () => {
expect(source).not.toContain(' {
+ const source = readFileSync(new URL('./explain/ExplainWorkbench.tsx', import.meta.url), 'utf8')
+
+ expect(source).toContain('wrapperClassName="gn-explain-report-spinner"')
+ expect(source).toMatch(
+ /\.gn-explain-report-spinner > \.ant-spin-container \{[^}]*height: 100%;[^}]*min-height: 0;[^}]*display: flex;[^}]*flex-direction: column;/s,
+ )
+ expect(source).toMatch(
+ /\.gn-explain-report-shell \{[^}]*flex: 1 1 auto;[^}]*min-height: 0;[^}]*display: flex;[^}]*flex-direction: column;/s,
+ )
+ })
+
it('keeps the editor draft separate from the SQL submitted for diagnosis', () => {
const source = readFileSync(new URL('./explain/SqlAnalysisWorkbench.tsx', import.meta.url), 'utf8')
diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx
index 5d29f4d3..6a4d2801 100644
--- a/frontend/src/components/QueryEditor.tsx
+++ b/frontend/src/components/QueryEditor.tsx
@@ -257,7 +257,7 @@ const buildQueryEditorInlineMemoryEntries = ({
});
sqlLogs.forEach((log) => {
- if (log.status !== 'success') {
+ if (log.status !== 'success' || log.category === 'transaction') {
return;
}
if (!matchesQueryEditorInlineMemoryDb(currentDb, log.dbName)) {
@@ -1553,7 +1553,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const binding = triggerSqlAiCompletionShortcutBinding;
const keyBinding = binding?.enabled && binding.combo
- ? comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode)
+ ? comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform)
: null;
triggerSqlAiCompletionActionRef.current = editor.addAction({
id: 'gonavi.triggerSqlAiCompletion',
@@ -1565,7 +1565,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
triggerAiInlineCompletionRef.current?.();
},
});
- }, [triggerSqlAiCompletionShortcutBinding]);
+ }, [activeShortcutPlatform, triggerSqlAiCompletionShortcutBinding]);
useEffect(() => {
// Prefer remount session cache (detach/attach); otherwise follow tab draft flag.
if (restoredResultSessionRef.current && restoredResultSessionRef.current.isResultPanelVisible !== undefined) {
@@ -1621,6 +1621,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
: 0;
const {
activatePendingSqlTransaction,
+ appendPendingSqlTransactionExecution,
autoCommitRemainingSeconds: sqlEditorAutoCommitRemainingSeconds,
finishPendingSqlTransaction,
pendingSqlTransaction,
@@ -1629,6 +1630,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
tabId: tab.id,
translate: (key, params) => translate(key, params),
});
+ const handleFinishPendingSqlTransaction = useCallback(async (action: 'commit' | 'rollback') => {
+ await finishPendingSqlTransaction(action, 'manual');
+ handleShowSqlExecutionLog('open');
+ }, [finishPendingSqlTransaction, handleShowSqlExecutionLog]);
const autoFetchVisible = useAutoFetchVisibility();
useEffect(() => {
@@ -4496,7 +4501,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const runBinding = runQueryShortcutBinding;
if (runBinding?.enabled && runBinding.combo) {
const keyBinding = comboToMonacoKeyBinding(
- runBinding.combo, monaco.KeyMod, monaco.KeyCode
+ runBinding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
);
if (keyBinding) {
runQueryActionRef.current = editor.addAction({
@@ -4516,7 +4521,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const selectStatementBinding = selectCurrentStatementShortcutBinding;
if (selectStatementBinding?.enabled && selectStatementBinding.combo) {
const keyBinding = comboToMonacoKeyBinding(
- selectStatementBinding.combo, monaco.KeyMod, monaco.KeyCode
+ selectStatementBinding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
);
if (keyBinding) {
selectCurrentStatementActionRef.current = editor.addAction({
@@ -4531,6 +4536,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const macFindWithSelectionGuardKeyBinding = activeShortcutPlatform === 'mac'
? comboToMonacoKeyBinding(
QUERY_EDITOR_MAC_FIND_WITH_SELECTION_COMBO, monaco.KeyMod, monaco.KeyCode,
+ activeShortcutPlatform,
)
: null;
if (macFindWithSelectionGuardKeyBinding) {
@@ -4556,6 +4562,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
if (duplicateLineBinding?.enabled && duplicateLineBinding.combo) {
const keyBinding = comboToMonacoKeyBinding(
duplicateLineBinding.combo, monaco.KeyMod, monaco.KeyCode,
+ activeShortcutPlatform,
);
if (keyBinding) {
duplicateCurrentLineActionRef.current = editor.addAction({
@@ -4570,7 +4577,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const saveBinding = saveQueryShortcutBinding;
if (saveBinding?.enabled && saveBinding.combo) {
const keyBinding = comboToMonacoKeyBinding(
- saveBinding.combo, monaco.KeyMod, monaco.KeyCode
+ saveBinding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
);
if (keyBinding) {
saveQueryActionRef.current = editor.addAction({
@@ -4585,7 +4592,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
const findInEditorKeyBinding = comboToMonacoKeyBinding(
- findInEditorShortcutCombo, monaco.KeyMod, monaco.KeyCode
+ findInEditorShortcutCombo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
);
if (findInEditorKeyBinding) {
findInEditorActionRef.current = editor.addAction({
@@ -4601,7 +4608,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const formatBinding = formatSqlShortcutBinding;
if (formatBinding?.enabled && formatBinding.combo) {
const keyBinding = comboToMonacoKeyBinding(
- formatBinding.combo, monaco.KeyMod, monaco.KeyCode
+ formatBinding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
);
if (keyBinding) {
formatSqlActionRef.current = editor.addAction({
@@ -4620,7 +4627,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const toggleResultsBinding = toggleQueryResultsPanelShortcutBinding;
if (toggleResultsBinding?.enabled && toggleResultsBinding.combo) {
const keyBinding = comboToMonacoKeyBinding(
- toggleResultsBinding.combo, monaco.KeyMod, monaco.KeyCode
+ toggleResultsBinding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
);
if (keyBinding) {
toggleQueryResultsPanelActionRef.current = editor.addAction({
@@ -6486,14 +6493,27 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return;
}
- if (useManagedTransaction && res.transactionPending && res.transactionId) {
- activatePendingSqlTransaction({
- id: String(res.transactionId),
- commitMode: sqlEditorCommitMode,
- autoCommitDelayMs: sqlEditorAutoCommitDelayMs,
- createdAt: Date.now(),
- statementCount: managedTransactionStatementCount,
- });
+ if (res.transactionPending && res.transactionId) {
+ const transactionId = String(res.transactionId);
+ if (useManagedTransaction) {
+ activatePendingSqlTransaction({
+ id: transactionId,
+ commitMode: sqlEditorCommitMode,
+ autoCommitDelayMs: sqlEditorAutoCommitDelayMs,
+ createdAt: Date.now(),
+ statementCount: managedTransactionStatementCount,
+ dbType: normalizedDbType,
+ dbName: currentDb,
+ statements: sourceStatements,
+ executionDurationMs: duration,
+ });
+ } else {
+ appendPendingSqlTransactionExecution({
+ transactionId,
+ statements: sourceStatements,
+ durationMs: duration,
+ });
+ }
}
// res.data 是 ResultSetData[] 数组
@@ -6897,7 +6917,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const binding = runQueryShortcutBinding;
if (!binding?.enabled || !binding.combo) return;
- const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode);
+ const keyBinding = comboToMonacoKeyBinding(
+ binding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
+ );
if (keyBinding) {
runQueryActionRef.current = editor.addAction({
id: 'gonavi.runQuery',
@@ -6918,7 +6940,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
runQueryActionRef.current = null;
}
};
- }, [languagePreference, runQueryShortcutBinding]);
+ }, [activeShortcutPlatform, languagePreference, runQueryShortcutBinding]);
useEffect(() => {
if (selectCurrentStatementActionRef.current) {
@@ -6936,7 +6958,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const binding = selectCurrentStatementShortcutBinding;
if (binding?.enabled && binding.combo) {
- const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode);
+ const keyBinding = comboToMonacoKeyBinding(
+ binding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
+ );
if (keyBinding) {
selectCurrentStatementActionRef.current = editor.addAction({
id: 'gonavi.selectCurrentStatement',
@@ -6952,6 +6976,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
QUERY_EDITOR_MAC_FIND_WITH_SELECTION_COMBO,
monaco.KeyMod,
monaco.KeyCode,
+ activeShortcutPlatform,
)
: null;
if (macFindWithSelectionGuardKeyBinding) {
@@ -6998,7 +7023,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const binding = duplicateCurrentLineShortcutBinding;
if (!binding?.enabled || !binding.combo) return;
- const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode);
+ const keyBinding = comboToMonacoKeyBinding(
+ binding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
+ );
if (keyBinding) {
duplicateCurrentLineActionRef.current = editor.addAction({
id: 'gonavi.duplicateCurrentLine',
@@ -7014,7 +7041,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
duplicateCurrentLineActionRef.current = null;
}
};
- }, [duplicateCurrentLineShortcutBinding, handleDuplicateCurrentLine, languagePreference]);
+ }, [activeShortcutPlatform, duplicateCurrentLineShortcutBinding, handleDuplicateCurrentLine, languagePreference]);
useEffect(() => {
if (saveQueryActionRef.current) {
@@ -7029,7 +7056,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const binding = saveQueryShortcutBinding;
if (!binding?.enabled || !binding.combo) return;
- const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode);
+ const keyBinding = comboToMonacoKeyBinding(
+ binding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
+ );
if (keyBinding) {
saveQueryActionRef.current = editor.addAction({
id: 'gonavi.saveQuery',
@@ -7047,7 +7076,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
saveQueryActionRef.current = null;
}
};
- }, [languagePreference, saveQueryShortcutBinding]);
+ }, [activeShortcutPlatform, languagePreference, saveQueryShortcutBinding]);
useEffect(() => {
if (findInEditorActionRef.current) {
@@ -7063,6 +7092,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
findInEditorShortcutCombo,
monaco.KeyMod,
monaco.KeyCode,
+ activeShortcutPlatform,
);
if (keyBinding) {
findInEditorActionRef.current = editor.addAction({
@@ -7081,7 +7111,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
findInEditorActionRef.current = null;
}
};
- }, [findInEditorShortcutCombo, languagePreference]);
+ }, [activeShortcutPlatform, findInEditorShortcutCombo, languagePreference]);
useEffect(() => {
if (formatSqlActionRef.current) {
@@ -7096,7 +7126,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const binding = formatSqlShortcutBinding;
if (!binding?.enabled || !binding.combo) return;
- const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode);
+ const keyBinding = comboToMonacoKeyBinding(
+ binding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
+ );
if (keyBinding) {
formatSqlActionRef.current = editor.addAction({
id: 'gonavi.formatSql',
@@ -7114,7 +7146,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
formatSqlActionRef.current = null;
}
};
- }, [languagePreference, formatSqlShortcutBinding]);
+ }, [activeShortcutPlatform, languagePreference, formatSqlShortcutBinding]);
useEffect(() => {
const editor = editorRef.current;
@@ -7144,7 +7176,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const binding = toggleQueryResultsPanelShortcutBinding;
if (!binding?.enabled || !binding.combo) return;
- const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode);
+ const keyBinding = comboToMonacoKeyBinding(
+ binding.combo, monaco.KeyMod, monaco.KeyCode, activeShortcutPlatform,
+ );
if (keyBinding) {
toggleQueryResultsPanelActionRef.current = editor.addAction({
id: 'gonavi.toggleQueryResultsPanel',
@@ -7160,7 +7194,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
toggleQueryResultsPanelActionRef.current = null;
}
};
- }, [languagePreference, toggleQueryResultsPanelShortcutBinding, toggleResultPanelVisibility]);
+ }, [activeShortcutPlatform, languagePreference, toggleQueryResultsPanelShortcutBinding, toggleResultPanelVisibility]);
useEffect(() => {
const handleRunActiveQuery = (event: Event) => {
@@ -7937,7 +7971,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
darkMode={darkMode}
transaction={pendingSqlTransaction}
autoCommitRemainingSeconds={sqlEditorAutoCommitRemainingSeconds}
- onFinish={(action) => void finishPendingSqlTransaction(action, 'manual')}
+ onFinish={(action) => void handleFinishPendingSqlTransaction(action)}
/>
);
const queryEditorStageStyle: React.CSSProperties = isResultPanelVisible
diff --git a/frontend/src/components/QueryEditorTransactionToolbar.tsx b/frontend/src/components/QueryEditorTransactionToolbar.tsx
index d3077760..c598f020 100644
--- a/frontend/src/components/QueryEditorTransactionToolbar.tsx
+++ b/frontend/src/components/QueryEditorTransactionToolbar.tsx
@@ -11,6 +11,10 @@ export type PendingSqlEditorTransaction = {
createdAt: number;
autoCommitDueAt?: number | null;
statementCount?: number;
+ dbType?: string;
+ dbName?: string;
+ statements?: string[];
+ executionDurationMs?: number;
};
type QueryEditorTransactionToolbarProps = {
diff --git a/frontend/src/components/Sidebar.message-publish.test.tsx b/frontend/src/components/Sidebar.message-publish.test.tsx
index 4cb589b5..ecc3d629 100644
--- a/frontend/src/components/Sidebar.message-publish.test.tsx
+++ b/frontend/src/components/Sidebar.message-publish.test.tsx
@@ -3,22 +3,27 @@ import { readFileSync } from 'node:fs';
const sidebarSource = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const contextMenuSource = readFileSync(new URL('./V2TableContextMenu.tsx', import.meta.url), 'utf8');
+const legacyMenuSource = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
+const v2ActionSource = readFileSync(new URL('./sidebar/useSidebarV2ActionHandlers.tsx', import.meta.url), 'utf8');
const modalSource = readFileSync(new URL('./MessagePublishModal.tsx', import.meta.url), 'utf8');
describe('Sidebar Kafka publish entry', () => {
it('adds a Kafka topic publish action in both legacy and v2 table menus', () => {
- expect(sidebarSource).toContain("key: 'publish-message'");
- expect(sidebarSource).toContain("label: t('message_publish_modal.title')");
- expect(sidebarSource).toContain('openMessagePublishModal(node)');
+ expect(legacyMenuSource).toContain("key: 'publish-message'");
+ expect(legacyMenuSource).toContain("label: t('message_publish_modal.title')");
+ expect(legacyMenuSource).toContain('openMessagePublishModal(node)');
expect(contextMenuSource).toContain("| 'publish-message'");
expect(contextMenuSource).toContain("title: t('message_publish_modal.title')");
+ expect(v2ActionSource).toContain("case 'publish-message'");
+ expect(v2ActionSource).toContain('openMessagePublishModal(node)');
expect(contextMenuSource).not.toContain("title: '测试发送消息'");
});
- it('renders the dedicated message publish modal and executes DBQuery through the encoder', () => {
+ it('renders the dedicated message publish modal and executes an audited user action through the encoder', () => {
expect(sidebarSource).toContain('
+
)}
diff --git a/frontend/src/components/TabManager.tsx b/frontend/src/components/TabManager.tsx
index 788fe227..3f4d6545 100644
--- a/frontend/src/components/TabManager.tsx
+++ b/frontend/src/components/TabManager.tsx
@@ -44,6 +44,7 @@ const getTabKindLabel = (tab: TabData): string => {
if (tab.type === 'table-export') return t('tab_manager.kind_badge.table_export');
if (tab.type === 'sql-file-execution') return t('sidebar.sql_file_exec.title');
if (tab.type === 'sql-analysis') return t('tab_manager.kind_badge.sql_analysis');
+ if (tab.type === 'sql-audit') return t('tab_manager.kind_badge.sql_audit');
if (tab.type.startsWith('redis')) return t('tab_manager.kind_badge.redis');
if (tab.type.startsWith('jvm')) return t('tab_manager.kind_badge.jvm');
if (tab.type === 'trigger') return t('tab_manager.kind_badge.trigger');
@@ -69,6 +70,7 @@ const getTabKindTooltipLabel = (tab: TabData): string => {
if (tab.type === 'table-export') return t('tab_manager.hover.kind.table_export');
if (tab.type === 'sql-file-execution') return t('sidebar.sql_file_exec.title');
if (tab.type === 'sql-analysis') return t('tab_manager.hover.kind.sql_analysis');
+ if (tab.type === 'sql-audit') return t('tab_manager.hover.kind.sql_audit');
if (tab.type === 'redis-keys') return t('tab_manager.hover.kind.redis_keys');
if (tab.type === 'redis-command') return t('tab_manager.hover.kind.redis_command');
if (tab.type === 'redis-monitor') return t('tab_manager.hover.kind.redis_monitor');
@@ -100,7 +102,7 @@ const getTabObjectLabel = (tab: TabData): string => {
if (tab.triggerName) return tab.triggerName;
if (tab.resourcePath) return tab.resourcePath;
if (tab.filePath) return tab.filePath;
- if (tab.type === 'sql-analysis') return tab.title;
+ if (tab.type === 'sql-analysis' || tab.type === 'sql-audit') return tab.title;
if (tab.type.startsWith('redis')) return `db${tab.redisDB ?? 0}`;
return '';
};
diff --git a/frontend/src/components/TableDesigner.tsx b/frontend/src/components/TableDesigner.tsx
index 33bf0c6b..737b0282 100644
--- a/frontend/src/components/TableDesigner.tsx
+++ b/frontend/src/components/TableDesigner.tsx
@@ -8,7 +8,7 @@ import { CSS } from '@dnd-kit/utilities';
import Editor from './MonacoEditor';
import { TabData, ColumnDefinition, IndexDefinition, ForeignKeyDefinition, TriggerDefinition } from '../types';
import { useStore } from '../store';
-import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App';
+import { DBGetColumns, DBGetIndexes, DBQueryAudited, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App';
import { hasIndexFormChanged, normalizeIndexFormFromRow, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils';
import { buildIndexCreateSqlPreview } from './tableDesignerIndexSql';
import { buildAlterTablePreviewSql, buildCreateTablePreviewSql, hasAlterTableDraftChanges, type StarRocksCreateTableOptions, type StarRocksDistributionType, type StarRocksKeyModel, type StarRocksTableKind } from './tableDesignerSchemaSql';
@@ -1209,7 +1209,7 @@ ${selectedTrigger.statement}`;
const dropSql = buildDropTriggerSql(selectedTrigger.name);
try {
- const res = await DBQuery(buildRpcConnectionConfig(config) as any, tab.dbName || '', dropSql);
+ const res = await DBQueryAudited(buildRpcConnectionConfig(config) as any, tab.dbName || '', dropSql, 'table_designer');
if (res.success) {
message.success(t('table_designer.message.trigger_deleted', undefined, i18nLanguage));
setSelectedTrigger(null);
@@ -1246,7 +1246,7 @@ ${selectedTrigger.statement}`;
// 如果是编辑模式,先删除旧触发器
if (triggerEditMode === 'edit' && selectedTrigger) {
const dropSql = buildDropTriggerSql(selectedTrigger.name);
- const dropRes = await DBQuery(buildRpcConnectionConfig(config) as any, tab.dbName || '', dropSql);
+ const dropRes = await DBQueryAudited(buildRpcConnectionConfig(config) as any, tab.dbName || '', dropSql, 'table_designer');
if (!dropRes.success) {
message.error(t('table_designer.message.drop_old_trigger_failed', { detail: dropRes.message }, i18nLanguage));
setTriggerExecuting(false);
@@ -1255,7 +1255,7 @@ ${selectedTrigger.statement}`;
}
// 执行创建语句
- const res = await DBQuery(buildRpcConnectionConfig(config) as any, tab.dbName || '', triggerEditSql);
+ const res = await DBQueryAudited(buildRpcConnectionConfig(config) as any, tab.dbName || '', triggerEditSql, 'table_designer');
if (res.success) {
message.success(triggerEditMode === 'create'
? t('table_designer.message.trigger_created', undefined, i18nLanguage)
@@ -1761,7 +1761,7 @@ ${selectedTrigger.statement}`;
const sql = buildCreateTableSql(copyTableName.trim(), selectedColumns, copyCharset, copyCollation);
setCopyExecuting(true);
try {
- const res = await DBQuery(buildRpcConnectionConfig(config) as any, tab.dbName || '', sql);
+ const res = await DBQueryAudited(buildRpcConnectionConfig(config) as any, tab.dbName || '', sql, 'table_designer');
if (res.success) {
message.success(t('table_designer.message.columns_copied_to_new_table', { count: selectedColumns.length, table: copyTableName.trim() }, i18nLanguage));
setIsCopyColumnsModalOpen(false);
@@ -1790,7 +1790,7 @@ ${selectedTrigger.statement}`;
const statements = splitSchemaExecutionStatements(sqlText);
for (let i = 0; i < statements.length; i++) {
const stmt = normalizeSchemaStatementForExecution(statements[i], dbType);
- const res = await DBQuery(buildRpcConnectionConfig(config) as any, tab.dbName || '', stmt);
+ const res = await DBQueryAudited(buildRpcConnectionConfig(config) as any, tab.dbName || '', stmt, 'table_designer');
if (!res.success) {
const prefix = statements.length > 1
? t('table_designer.message.statement_execution_failed_prefix', { current: i + 1, total: statements.length }, i18nLanguage)
diff --git a/frontend/src/components/WorkbenchTabContent.tsx b/frontend/src/components/WorkbenchTabContent.tsx
index 3bd30e81..221b9af8 100644
--- a/frontend/src/components/WorkbenchTabContent.tsx
+++ b/frontend/src/components/WorkbenchTabContent.tsx
@@ -17,6 +17,7 @@ import JVMAuditViewer from './JVMAuditViewer';
import JVMDiagnosticConsole from './JVMDiagnosticConsole';
import JVMMonitoringDashboard from './JVMMonitoringDashboard';
import SqlAnalysisWorkbench from './explain/SqlAnalysisWorkbench';
+import SqlAuditWorkbench from './audit/SqlAuditWorkbench';
export const WorkbenchTabContent: React.FC<{ tab: TabData; isActive: boolean }> = React.memo(({ tab, isActive }) => {
if (tab.type === 'query') {
@@ -55,6 +56,9 @@ export const WorkbenchTabContent: React.FC<{ tab: TabData; isActive: boolean }>
if (tab.type === 'sql-analysis') {
return