diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 21debb6..0c8bdfd 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -1226,6 +1226,7 @@ interface DataGridProps { onApplyQuickWhereCondition?: (condition: string) => void; scrollSnapshot?: { top: number; left: number }; onScrollSnapshotChange?: (snapshot: { top: number; left: number }) => void; + toolbarExtraActions?: React.ReactNode; } type GridFilterCondition = FilterCondition & { @@ -1487,7 +1488,7 @@ const DataGrid: React.FC = ({ data, columnNames, loading, tableName, objectType = 'table', exportScope = 'table', dbName, connectionId, pkColumns = [], editLocator, readOnly = false, onReload, onSort, onPageChange, pagination, onRequestTotalCount, onCancelTotalCount, sortInfoExternal, showFilter, onToggleFilter, exportSqlWithFilter, onApplyFilter, appliedFilterConditions, quickWhereCondition, onApplyQuickWhereCondition, - scrollSnapshot, onScrollSnapshotChange + scrollSnapshot, onScrollSnapshotChange, toolbarExtraActions }) => { const connections = useStore(state => state.connections); const addTab = useStore(state => state.addTab); @@ -7432,6 +7433,7 @@ const DataGrid: React.FC = ({ aiShortcutLabel={aiShortcutLabel} legacyAiButtonStyle={legacyAiButtonStyle} paginationTotalCountLoading={pagination?.totalCountLoading} + toolbarExtraActions={toolbarExtraActions} filterConditions={filterConditions} sortInfo={sortInfo} displayColumnNames={displayColumnNames} diff --git a/frontend/src/components/DataGridToolbarFrame.tsx b/frontend/src/components/DataGridToolbarFrame.tsx index c3384a4..9cc70d0 100644 --- a/frontend/src/components/DataGridToolbarFrame.tsx +++ b/frontend/src/components/DataGridToolbarFrame.tsx @@ -73,6 +73,7 @@ export interface DataGridToolbarFrameProps { aiShortcutLabel: string; legacyAiButtonStyle?: React.CSSProperties; paginationTotalCountLoading?: boolean; + toolbarExtraActions?: React.ReactNode; filterConditions: GridFilterCondition[]; sortInfo: GridSortInfo[]; displayColumnNames: string[]; @@ -166,6 +167,7 @@ const DataGridToolbarFrame: React.FC = ({ aiShortcutLabel, legacyAiButtonStyle, paginationTotalCountLoading, + toolbarExtraActions, filterConditions, sortInfo, displayColumnNames, @@ -410,6 +412,13 @@ const DataGridToolbarFrame: React.FC = ({ + {toolbarExtraActions && ( + <> + {renderToolbarDivider()} + {toolbarExtraActions} + + )} + {prefersManualTotalCount && ( <> {renderToolbarDivider()} diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 13733cb..5df034b 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -36,7 +36,12 @@ const storeState = vi.hoisted(() => ({ appearance: { uiVersion: 'legacy' as 'legacy' | 'v2' }, sqlFormatOptions: { keywordCase: 'upper' as const }, setSqlFormatOptions: vi.fn(), - queryOptions: { maxRows: 5000 }, + queryOptions: { + maxRows: 5000, + showColumnComment: true, + showColumnType: true, + showQueryResultsPanel: false, + }, setQueryOptions: vi.fn(), shortcutOptions: { runQuery: { @@ -51,6 +56,10 @@ const storeState = vi.hoisted(() => ({ mac: { enabled: true, combo: 'Meta+S' }, windows: { enabled: true, combo: 'Ctrl+S' }, }, + toggleQueryResultsPanel: { + mac: { enabled: true, combo: 'Meta+Shift+M' }, + windows: { enabled: true, combo: 'Ctrl+Shift+M' }, + }, }, activeTabId: 'tab-1', aiPanelVisible: false, @@ -250,7 +259,7 @@ vi.mock('@monaco-editor/react', () => ({ onMount?.(editorState.editor, { editor: { setTheme: vi.fn() }, KeyMod: { CtrlCmd: 2048, WinCtrl: 256 }, - KeyCode: { KeyQ: 81, KeyS: 83 }, + KeyCode: { KeyM: 77, KeyQ: 81, KeyS: 83 }, languages: { CompletionItemKind: { Keyword: 1, Function: 2, Field: 3 }, CompletionItemInsertTextRule: { InsertAsSnippet: 1 }, @@ -298,7 +307,11 @@ vi.mock('@monaco-editor/react', () => ({ vi.mock('./DataGrid', () => ({ default: (props: any) => { dataGridState.latestProps = props; - return
; + return ( +
+ {props.toolbarExtraActions ?? null} +
+ ); }, GONAVI_ROW_KEY: '__gonavi_row_key__', })); @@ -314,6 +327,8 @@ vi.mock('@ant-design/icons', () => { StopOutlined: Icon, RobotOutlined: Icon, DatabaseOutlined: Icon, + EyeOutlined: Icon, + EyeInvisibleOutlined: Icon, }; }); @@ -352,24 +367,27 @@ vi.mock('antd', () => { ), Tooltip: ({ children }: any) => <>{children}, Select: () => null, - Tabs: ({ activeKey, items, onChange }: any) => { + Tabs: ({ activeKey, items, onChange, tabBarExtraContent }: any) => { const resolvedActiveKey = tabsState.activeKey ?? activeKey ?? items?.[0]?.key; const activeItem = items?.find((item: any) => item.key === resolvedActiveKey) || items?.[0]; return (
-
{items?.map((item: any) => ( - - ))}
+
+ {items?.map((item: any) => ( + + ))} + {tabBarExtraContent?.right ?? null} +
{activeItem?.children}
); @@ -425,6 +443,34 @@ describe('QueryEditor external SQL save', () => { storeState.saveQuery.mockReset(); storeState.savedQueries = []; storeState.activeTabId = 'tab-1'; + storeState.queryOptions = { + maxRows: 5000, + showColumnComment: true, + showColumnType: true, + showQueryResultsPanel: false, + }; + storeState.shortcutOptions = { + runQuery: { + mac: { enabled: false, combo: '' }, + windows: { enabled: false, combo: '' }, + }, + selectCurrentStatement: { + mac: { enabled: false, combo: '' }, + windows: { enabled: false, combo: '' }, + }, + saveQuery: { + mac: { enabled: true, combo: 'Meta+S' }, + windows: { enabled: true, combo: 'Ctrl+S' }, + }, + toggleQueryResultsPanel: { + mac: { enabled: true, combo: 'Meta+Shift+M' }, + windows: { enabled: true, combo: 'Ctrl+Shift+M' }, + }, + }; + storeState.setQueryOptions.mockReset(); + storeState.setQueryOptions.mockImplementation((options: Record) => { + storeState.queryOptions = { ...storeState.queryOptions, ...options }; + }); messageApi.success.mockReset(); messageApi.error.mockReset(); messageApi.warning.mockReset(); @@ -486,6 +532,173 @@ describe('QueryEditor external SQL save', () => { expect(editorState.value).toBe('SELECT * FROM '); }); + it('keeps the query results panel hidden by default on first entry', async () => { + storeState.appearance.uiVersion = 'v2'; + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + expect(textContent(renderer.toJSON())).not.toContain('等待执行 SQL'); + }); + + it('shows the empty query results panel after toggling the results button', async () => { + storeState.appearance.uiVersion = 'v2'; + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + await act(async () => { + findButton(renderer, '结果').props.onClick(); + }); + + expect(textContent(renderer.toJSON())).toContain('等待执行 SQL'); + expect(storeState.setQueryOptions).toHaveBeenCalledWith({ showQueryResultsPanel: true }); + }); + + it('hides the expanded empty query results panel from the inline hide action', async () => { + storeState.appearance.uiVersion = 'v2'; + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + await act(async () => { + findButton(renderer, '结果').props.onClick(); + }); + expect(textContent(renderer.toJSON())).toContain('等待执行 SQL'); + + await act(async () => { + findButton(renderer, '隐藏').props.onClick(); + }); + + expect(textContent(renderer.toJSON())).not.toContain('等待执行 SQL'); + expect(storeState.setQueryOptions).toHaveBeenLastCalledWith({ showQueryResultsPanel: false }); + }); + + it('auto expands the query results panel after a successful execution returns rows', async () => { + storeState.appearance.uiVersion = 'v2'; + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: [{ columns: ['value'], rows: [{ value: 1 }] }], + }); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + expect(textContent(renderer.toJSON())).not.toContain('结果 1'); + + await act(async () => { + await findButton(renderer, '运行').props.onClick(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(textContent(renderer.toJSON())).toContain('结果 1'); + expect(storeState.setQueryOptions).toHaveBeenCalledWith({ showQueryResultsPanel: true }); + }); + + it('keeps the inline hide action available after query results render rows', async () => { + storeState.appearance.uiVersion = 'v2'; + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: [{ columns: ['value'], rows: [{ value: 1 }] }], + }); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + await act(async () => { + await findButton(renderer, '运行').props.onClick(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(textContent(renderer.toJSON())).toContain('结果 1'); + + await act(async () => { + findButton(renderer, '隐藏').props.onClick(); + }); + + expect(textContent(renderer.toJSON())).not.toContain('结果 1'); + expect(storeState.setQueryOptions).toHaveBeenLastCalledWith({ showQueryResultsPanel: false }); + }); + + it('toggles the query results panel with Ctrl/Cmd+Shift+M', async () => { + storeState.appearance.uiVersion = 'v2'; + + 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(), + requestAnimationFrame: vi.fn((callback: FrameRequestCallback) => { + callback(0); + return 1; + }), + cancelAnimationFrame: vi.fn(), + innerHeight: 900, + }); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + const toggleAction = editorState.editor.addAction.mock.calls + .map((call: any[]) => call[0]) + .find((action: any) => action?.id === 'gonavi.toggleQueryResultsPanel'); + expect(toggleAction).toMatchObject({ + label: 'GoNavi: 切换结果区', + }); + expect(toggleAction?.keybindings?.[0]).toBeGreaterThan(0); + + const isMacRuntime = /(Mac|iPhone|iPad|iPod)/i.test(`${navigator.platform || ''} ${navigator.userAgent || ''}`); + const createToggleEvent = () => ({ + ctrlKey: !isMacRuntime, + metaKey: isMacRuntime, + altKey: false, + shiftKey: true, + key: 'm', + target: null, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + }); + + const firstEvent = createToggleEvent(); + await act(async () => { + windowListeners.keydown?.forEach((listener) => listener(firstEvent)); + }); + + expect(firstEvent.preventDefault).toHaveBeenCalled(); + expect(firstEvent.stopPropagation).toHaveBeenCalled(); + expect(textContent(renderer.toJSON())).toContain('等待执行 SQL'); + + const secondEvent = createToggleEvent(); + await act(async () => { + windowListeners.keydown?.forEach((listener) => listener(secondEvent)); + }); + + expect(secondEvent.preventDefault).toHaveBeenCalled(); + expect(secondEvent.stopPropagation).toHaveBeenCalled(); + expect(textContent(renderer.toJSON())).not.toContain('等待执行 SQL'); + }); + it('keeps table name completion available after typing in a fresh query tab', async () => { let renderer!: ReactTestRenderer; autoFetchState.visible = true; @@ -3033,6 +3246,7 @@ describe('QueryEditor external SQL save', () => { const moveListeners: Array<(event: MouseEvent) => void> = []; const upListeners: Array<() => void> = []; const frameCallbacks: FrameRequestCallback[] = []; + storeState.queryOptions.showQueryResultsPanel = true; vi.mocked(document.addEventListener).mockImplementation((type: string, listener: any) => { if (type === 'mousemove') moveListeners.push(listener); if (type === 'mouseup') upListeners.push(listener); diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 51d4d58..da40aaf 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import Editor, { type OnMount } from './MonacoEditor'; import { Button, message, Modal, Input, Form, Dropdown, MenuProps, Tooltip, Select, Tabs } from 'antd'; -import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined, CloseOutlined, StopOutlined, RobotOutlined } from '@ant-design/icons'; +import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined, CloseOutlined, StopOutlined, RobotOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; import { format } from 'sql-formatter'; import { v4 as uuidv4 } from 'uuid'; import { TabData, ColumnDefinition, IndexDefinition } from '../types'; @@ -1977,6 +1977,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const runQueryActionRef = useRef(null); const selectCurrentStatementActionRef = useRef(null); const saveQueryActionRef = useRef(null); + const toggleQueryResultsPanelActionRef = useRef(null); const lastExternalQueryRef = useRef(getTabQueryValue(tab)); const lastEditorCursorPositionRef = useRef(null); const lastHoverTargetPositionRef = useRef<{ lineNumber: number; column: number } | null>(null); @@ -2021,6 +2022,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const setSqlFormatOptions = useStore(state => state.setSqlFormatOptions); const queryOptions = useStore(state => state.queryOptions); const setQueryOptions = useStore(state => state.setQueryOptions); + const [isResultPanelVisible, setIsResultPanelVisible] = useState(Boolean(queryOptions?.showQueryResultsPanel)); const shortcutOptions = useStore(state => state.shortcutOptions); const activeShortcutPlatform = getShortcutPlatform(isMacLikePlatform()); const runQueryShortcutBinding = useMemo( @@ -2035,10 +2037,28 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc () => resolveShortcutBinding(shortcutOptions, 'saveQuery', activeShortcutPlatform), [activeShortcutPlatform, shortcutOptions], ); + const toggleQueryResultsPanelShortcutBinding = useMemo( + () => resolveShortcutBinding(shortcutOptions, 'toggleQueryResultsPanel', activeShortcutPlatform), + [activeShortcutPlatform, shortcutOptions], + ); const primaryShortcutModifierLabel = useMemo( () => getShortcutPrimaryModifierDisplayLabel(activeShortcutPlatform), [activeShortcutPlatform], ); + useEffect(() => { + setIsResultPanelVisible(Boolean(queryOptions?.showQueryResultsPanel)); + }, [queryOptions?.showQueryResultsPanel]); + const updateResultPanelVisibility = useCallback((visible: boolean) => { + setIsResultPanelVisible(visible); + setQueryOptions({ showQueryResultsPanel: visible }); + }, [setQueryOptions]); + const toggleResultPanelVisibility = useCallback(() => { + setIsResultPanelVisible((previousVisible) => { + const nextVisible = !previousVisible; + setQueryOptions({ showQueryResultsPanel: nextVisible }); + return nextVisible; + }); + }, [setQueryOptions]); const autoFetchVisible = useAutoFetchVisibility(); const currentSavedQuery = useMemo(() => { @@ -3116,6 +3136,21 @@ 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 + ); + if (keyBinding) { + toggleQueryResultsPanelActionRef.current = editor.addAction({ + id: 'gonavi.toggleQueryResultsPanel', + label: 'GoNavi: 切换结果区', + keybindings: [keyBinding.keyMod | keyBinding.keyCode], + run: toggleResultPanelVisibility, + }); + } + } + // HMR 重载或测试重置时,以全局状态为准,避免本地闭包状态和 provider 列表不同步。 sqlCompletionRegistered = Boolean(_g.__gonaviSqlCompletionState.registered); sqlCompletionDisposables = _g.__gonaviSqlCompletionState.disposables; @@ -4109,6 +4144,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc if (shellConvert.recognized) { if (shellConvert.error) { const prefix = statements.length > 1 ? `第 ${idx + 1} 条语句执行失败:` : ''; + updateResultPanelVisibility(true); setExecutionError(formatSqlExecutionError(shellConvert.error, { prefix })); setResultSets([]); setActiveResultKey(''); @@ -4148,6 +4184,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }); if (!res.success) { const prefix = statements.length > 1 ? `第 ${idx + 1} 条语句执行失败:` : ''; + updateResultPanelVisibility(true); setExecutionError(formatSqlExecutionError(res.message, { prefix })); setResultSets([]); setActiveResultKey(''); @@ -4214,6 +4251,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc } } } + if (nextResultSets.length > 0) { + updateResultPanelVisibility(true); + } const shouldReplaceAllResults = didExecuteWholeEditor; setResultSets(prev => { const merged = mergeResultSets(prev, nextResultSets, shouldReplaceAllResults); @@ -4313,6 +4353,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc return; } + updateResultPanelVisibility(true); setExecutionError(formatSqlExecutionError(res.message)); setResultSets([]); setActiveResultKey(''); @@ -4424,6 +4465,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }); } + if (nextResultSets.length > 0) { + updateResultPanelVisibility(true); + } const shouldReplaceAllResults = didExecuteWholeEditor; setResultSets(prev => { const merged = mergeResultSets(prev, nextResultSets, shouldReplaceAllResults); @@ -4461,6 +4505,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc message: e.message, dbName: currentDb }); + updateResultPanelVisibility(true); setExecutionError(formattedError); setResultSets([]); setActiveResultKey(''); @@ -4659,6 +4704,37 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }; }, [saveQueryShortcutBinding]); + useEffect(() => { + if (toggleQueryResultsPanelActionRef.current) { + toggleQueryResultsPanelActionRef.current.dispose(); + toggleQueryResultsPanelActionRef.current = null; + } + + const editor = editorRef.current; + const monaco = monacoRef.current; + if (!editor || !monaco) return; + + const binding = toggleQueryResultsPanelShortcutBinding; + if (!binding?.enabled || !binding.combo) return; + + const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode); + if (keyBinding) { + toggleQueryResultsPanelActionRef.current = editor.addAction({ + id: 'gonavi.toggleQueryResultsPanel', + label: 'GoNavi: 切换结果区', + keybindings: [keyBinding.keyMod | keyBinding.keyCode], + run: toggleResultPanelVisibility, + }); + } + + return () => { + if (toggleQueryResultsPanelActionRef.current) { + toggleQueryResultsPanelActionRef.current.dispose(); + toggleQueryResultsPanelActionRef.current = null; + } + }; + }, [toggleQueryResultsPanelShortcutBinding, toggleResultPanelVisibility]); + useEffect(() => { const handleRunActiveQuery = () => { if (!isActive) { @@ -4907,6 +4983,39 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }; }, [isActive, saveQueryShortcutBinding, handleQuickSave]); + useEffect(() => { + const binding = toggleQueryResultsPanelShortcutBinding; + if (!binding?.enabled || !binding.combo) { + return; + } + + const handleToggleResultsShortcut = (event: KeyboardEvent) => { + if (!isActive) { + return; + } + if (!isShortcutMatch(event, binding.combo)) { + return; + } + + const editor = editorRef.current; + const targetNode = resolveEventTargetNode(event.target); + const editorHasFocus = !!editor?.hasTextFocus?.(); + const inQueryEditor = !!(targetNode && queryEditorRootRef.current?.contains(targetNode)); + if (!editorHasFocus && !inQueryEditor) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + toggleResultPanelVisibility(); + }; + + window.addEventListener('keydown', handleToggleResultsShortcut, true); + return () => { + window.removeEventListener('keydown', handleToggleResultsShortcut, true); + }; + }, [isActive, toggleQueryResultsPanelShortcutBinding, toggleResultPanelVisibility]); + useEffect(() => { const handleSaveActiveQuery = () => { if (!isActive) { @@ -5012,6 +5121,65 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }, ]; + const resolvedActiveResultKey = activeResultKey || resultSets[0]?.key || ''; + const activeResultSet = resultSets.find((rs) => rs.key === resolvedActiveResultKey) || null; + const activeResultUsesDataGrid = Boolean( + activeResultSet && + activeResultSet.resultType !== 'message' && + !(activeResultSet.columns.length === 1 && activeResultSet.columns[0] === 'affectedRows') + ); + const toggleQueryResultsPanelShortcutLabel = + toggleQueryResultsPanelShortcutBinding.enabled && toggleQueryResultsPanelShortcutBinding.combo + ? getShortcutDisplayLabel(toggleQueryResultsPanelShortcutBinding.combo, activeShortcutPlatform) + : ''; + const resultPanelHideTooltipTitle = toggleQueryResultsPanelShortcutLabel + ? `隐藏结果区(${toggleQueryResultsPanelShortcutLabel})` + : '隐藏结果区'; + + const resultPanelHideButton = ( + + + + ); + + const resultPanelTabsHideButton = ( + + + + ); + return (
-
+
= ({ tab, isAc + + + + , onClick: () => handleAIAction('generate') }, { key: 'ai-explain', label: '解释 SQL', icon: , onClick: () => handleAIAction('explain') }, @@ -5245,7 +5464,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
-
+
= ({ tab, isAc />
-
+ {isResultPanelVisible && ( +
+ )}
-
+ {isResultPanelVisible && ( +
{resultSets.length > 0 ? ( ({ key: rs.key, label: ( @@ -5427,6 +5654,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc editLocator={rs.editLocator} onReload={() => handleReloadResult(rs.key, rs.sql)} readOnly={rs.readOnly} + toolbarExtraActions={resolvedActiveResultKey === rs.key ? resultPanelToolbarHideButton : null} />
); @@ -5434,6 +5662,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }))} /> ) : executionError ? ( + <> +
+ 结果区 + {resultPanelHideButton} +
@@ -5462,7 +5695,13 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
+ ) : ( + <> +
+ 结果区 + {resultPanelHideButton} +
{isV2Ui && (
@@ -5471,8 +5710,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
)}
+ )}
+ )} { typeof raw.showColumnComment === "boolean" ? raw.showColumnComment : true; const showColumnType = typeof raw.showColumnType === "boolean" ? raw.showColumnType : true; + const showQueryResultsPanel = + typeof raw.showQueryResultsPanel === "boolean" ? raw.showQueryResultsPanel : false; if (!Number.isFinite(maxRows) || maxRows <= 0) { - return { maxRows: 5000, showColumnComment, showColumnType }; + return { maxRows: 5000, showColumnComment, showColumnType, showQueryResultsPanel }; } return { maxRows: Math.min(50000, Math.trunc(maxRows)), showColumnComment, showColumnType, + showQueryResultsPanel, }; }; @@ -1975,6 +1979,7 @@ export const useStore = create()( maxRows: 5000, showColumnComment: true, showColumnType: true, + showQueryResultsPanel: false, }, shortcutOptions: cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS), sqlSnippets: DEFAULT_SQL_SNIPPETS, diff --git a/frontend/src/v2-theme.css b/frontend/src/v2-theme.css index 184efb2..f64a9df 100644 --- a/frontend/src/v2-theme.css +++ b/frontend/src/v2-theme.css @@ -3277,6 +3277,18 @@ body[data-ui-version="v2"] .gn-v2-data-grid .gn-v2-ai-insight-button .gn-v2-tool color: var(--gn-info); } +body[data-ui-version="v2"] .gn-v2-data-grid .gn-v2-query-result-toolbar-hide { + border-color: var(--gn-br-1) !important; + background: var(--gn-bg-panel) !important; + color: var(--gn-fg-2) !important; + font-weight: 650 !important; +} + +body[data-ui-version="v2"] .gn-v2-data-grid .gn-v2-query-result-toolbar-hide:hover { + background: var(--gn-bg-hover) !important; + color: var(--gn-fg-1) !important; +} + body[data-ui-version="v2"] .gn-v2-smart-filter-panel { background: var(--gn-bg-panel-2) !important; border-top: 0.5px solid var(--gn-br-1); @@ -4920,6 +4932,12 @@ body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-n min-height: 38px; } +body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-extra-content { + display: inline-flex; + align-items: center; + padding-right: 8px; +} + body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab { width: auto !important; min-width: 0 !important; @@ -4984,6 +5002,15 @@ body[data-ui-version="v2"] .gn-v2-query-results .query-result-tab-count { color: var(--gn-fg-2); } +body[data-ui-version="v2"] .gn-v2-query-result-panel-header { + background: var(--gn-bg-panel-2); + border-bottom-color: var(--gn-br-1); +} + +body[data-ui-version="v2"] .gn-v2-query-result-panel-header .query-result-panel-header-title { + color: var(--gn-fg-3); +} + body[data-ui-version="v2"] .gn-v2-query-empty, body[data-ui-version="v2"] .gn-v2-query-success { display: flex;