diff --git a/frontend/src/components/DataGridShell.tsx b/frontend/src/components/DataGridShell.tsx index 811b38be..9b6d2532 100644 --- a/frontend/src/components/DataGridShell.tsx +++ b/frontend/src/components/DataGridShell.tsx @@ -714,7 +714,7 @@ const renderDataTableView = () => ( onClearFiltersAndSorts={clearAllFiltersAndSorts} /> -
+
{contextHolder} {exportProgressModal} { beforeEach(() => { storeState.sqlLogs = []; storeState.clearSqlLogs.mockClear(); + storeState.theme = "light"; + storeState.appearance = { enabled: true, opacity: 1, blur: 0, uiVersion: "legacy" }; }); it("renders log panel chrome in the active language", () => { @@ -198,4 +201,43 @@ describe("LogPanel i18n", () => { }); expect(onDiagnoseExecutionError).toHaveBeenCalledTimes(1); }); + + it("omits duplicate log chrome from the embedded log tab", () => { + storeState.sqlLogs = [{ + id: "log-embedded", + timestamp: Date.UTC(2026, 6, 30, 8, 0, 0), + sql: "SELECT 1", + status: "success", + duration: 3, + }]; + + const renderer = renderLogPanel({ variant: "embedded" }); + const renderedText = textContent(renderer.toJSON()); + + expect(renderedText).not.toContain("SQL execution log"); + expect(renderedText).not.toContain("Track execution status, duration, and errors for quick review."); + expect(renderer.root.findAll((node) => node.props?.title === "Clear logs")).toHaveLength(0); + expect(renderedText).toContain("SELECT 1"); + }); + + it("uses the shared SQL workbench background for the embedded log surface", () => { + storeState.appearance = { enabled: true, opacity: 1, blur: 0, uiVersion: "v2" }; + + const renderer = renderLogPanel({ variant: "embedded" }); + const embeddedPanel = renderer.root.findByProps({ className: "log-panel-embedded" }); + const scrollPanel = renderer.root.findByProps({ className: "log-panel-scroll" }); + const css = readV2ThemeCss(); + const embeddedTableCss = css.slice( + css.indexOf('body[data-ui-version="v2"] .log-panel-embedded .log-panel-table .ant-table {'), + css.indexOf('body[data-ui-version="v2"] .gn-v2-query-result-panel-header .query-result-panel-header-title'), + ); + + expect(embeddedPanel.props.style.background).toBe( + "var(--gn-query-workbench-bg, var(--gn-bg-panel-2))", + ); + expect(scrollPanel.props.style.padding).toBe("0 0 12px"); + expect(embeddedTableCss).toContain( + "background: var(--gn-query-workbench-bg, var(--gn-bg-panel-2)) !important;", + ); + }); }); diff --git a/frontend/src/components/LogPanel.tsx b/frontend/src/components/LogPanel.tsx index a7870f08..4f967255 100644 --- a/frontend/src/components/LogPanel.tsx +++ b/frontend/src/components/LogPanel.tsx @@ -71,7 +71,6 @@ const LogPanel: React.FC = ({ ? `rgba(255, 255, 255, ${Math.max(0.28, opacity * 0.48)})` : `rgba(0, 0, 0, ${Math.max(0.18, opacity * 0.36)})`; const isEmbedded = variant === 'embedded'; - const logCountLabel = sqlLogs.length.toLocaleString(); const columns = [ { @@ -118,7 +117,7 @@ const LogPanel: React.FC = ({ style={{ flex: 1, overflow: 'auto', - padding: isEmbedded ? '0 12px 12px' : '8px 10px 10px', + padding: isEmbedded ? '0 0 12px' : '8px 10px 10px', }} > {sqlLogs.length === 0 ? ( @@ -187,60 +186,18 @@ const LogPanel: React.FC = ({ if (isEmbedded) { return (
-
-
-
- -
-
-
- {t('log_panel.description')} -
-
- {logCountLabel} -
-
-
- -
{executionError && (
{ }; describe('GoNavi Monaco themes', () => { - it('defines bold SQL keyword colors that remain AA-readable across every built-in preset', () => { + it('defines bold SQL keyword colors that remain AA-readable on the shared workbench surface', () => { const defineTheme = vi.fn(); registerGonaviMonacoThemes({ editor: { defineTheme } } as never); @@ -56,7 +56,7 @@ describe('GoNavi Monaco themes', () => { for (const preset of BUILTIN_CUSTOM_THEME_PRESETS) { const keyword = (preset.baseMode === 'dark' ? darkRules : lightRules) .find((rule: any) => rule.token === 'keyword.sql'); - const background = readHexProperty(preset.css, '--gn-bg-input'); + const background = readHexProperty(preset.css, '--gn-bg-panel-2'); expect( contrastRatio(`#${keyword.foreground}`, background), `${preset.id} SQL keyword must contrast with its editor background`, diff --git a/frontend/src/components/QueryEditor.results-and-drop.test.tsx b/frontend/src/components/QueryEditor.results-and-drop.test.tsx index c93d0f17..4dece376 100644 --- a/frontend/src/components/QueryEditor.results-and-drop.test.tsx +++ b/frontend/src/components/QueryEditor.results-and-drop.test.tsx @@ -3487,11 +3487,30 @@ describe('QueryEditor external SQL save', () => { expect(textContent(renderer!.toJSON())).not.toContain('结果 1 (2)'); }); - it('keeps query result tabs compact, centered, and readable in v2 UI', () => { + it('keeps query result tabs flush, full-height, and readable in v2 UI', () => { const source = readFileSync(new URL('./QueryEditorResultsPanel.tsx', import.meta.url), 'utf8'); const css = readV2ThemeCss(); + const resultNavCss = css.slice( + css.indexOf('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav {'), + css.indexOf('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-extra-content {'), + ); + const resultTabCss = css.slice( + css.indexOf('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab {'), + css.indexOf('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-nav-list {'), + ); expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab {'); expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab-btn {'); + expect(resultNavCss).toContain('padding: 0 8px 0 0;'); + expect(resultNavCss).toContain('min-height: 46px;'); + expect(resultTabCss).toContain('height: 46px !important;'); + expect(resultTabCss).toContain('margin: 0 !important;'); + expect(resultTabCss).toContain('border-radius: 8px !important;'); + expect(css).toContain([ + 'body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-nav-wrap,', + 'body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-nav-list {', + ' min-height: 46px;', + '}', + ].join('\n')); expect(css).toContain('user-select: none;'); expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tab-text {'); }); @@ -3767,6 +3786,25 @@ describe('QueryEditor external SQL save', () => { const v2Renderer = renderResultsPanel(true); expect(v2Renderer.root.findAll((node) => node.props?.['data-log-panel'] === 'true')).toHaveLength(1); expect(v2Renderer.root.findAll((node) => node.props?.['data-tab-key'] === '__gonavi_sql_execution_log__')).toHaveLength(1); + const tabActions = v2Renderer.root.findByProps({ className: 'query-result-panel-tab-actions' }); + const actionButtons = tabActions.findAll((node) => node.type === 'button'); + const resultPanelStyles = v2Renderer.root.findAll((node) => node.type === 'style') + .map((node) => textContent(node)) + .join('\n'); + expect(actionButtons.map((node) => node.props.className)).toEqual([ + 'query-result-panel-clear query-result-panel-tab-action', + 'query-result-panel-hide query-result-panel-tab-action', + ]); + expect(resultPanelStyles).toContain( + '.query-result-panel-tab-actions { display: inline-flex; flex-direction: row;', + ); + expect(resultPanelStyles).toContain( + '.query-result-tabs .ant-tabs-extra-content .query-result-panel-tab-action { width: 28px; min-width: 28px; height: 28px !important; min-height: 28px !important; padding: 0 !important;', + ); + act(() => { + actionButtons[0].props.onClick(); + }); + expect(storeState.clearSqlLogs).toHaveBeenCalledTimes(1); v2Renderer.unmount(); const emptyV2Renderer = renderResultsPanel(true, 0); diff --git a/frontend/src/components/QueryEditorResultsPanel.tsx b/frontend/src/components/QueryEditorResultsPanel.tsx index ef3998f4..87ac858f 100644 --- a/frontend/src/components/QueryEditorResultsPanel.tsx +++ b/frontend/src/components/QueryEditorResultsPanel.tsx @@ -1,7 +1,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Button, Dropdown, Tabs, Tooltip, message, type MenuProps } from 'antd'; -import { BugOutlined, CloseOutlined, CopyOutlined, EyeInvisibleOutlined, PushpinOutlined, RobotOutlined } from '@ant-design/icons'; +import { BugOutlined, ClearOutlined, CloseOutlined, CopyOutlined, EyeInvisibleOutlined, PushpinOutlined, RobotOutlined } from '@ant-design/icons'; +import { useStore } from '../store'; import type { EditRowLocator } from '../utils/rowLocator'; import type { GridSortInfoItem } from '../utils/dataGridSort'; import type { QueryResultPaginationState } from '../utils/queryResultPagination'; @@ -164,6 +165,7 @@ const QueryEditorResultsPanel: React.FC = ({ }) => { const i18n = useOptionalI18n(); const t = i18n?.t ?? defaultTranslate; + const clearSqlLogs = useStore(state => state.clearSqlLogs); const globalHiddenColumns = useGlobalHiddenColumns(); const [draggingResultKey, setDraggingResultKey] = useState(null); const [detachDragPreview, setDetachDragPreview] = useState(null); @@ -696,11 +698,24 @@ const QueryEditorResultsPanel: React.FC = ({ const tabsHideButton = ( -