mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-16 03:29:55 +08:00
✨ feat(ai): 增强代码热点拆分诊断
This commit is contained in:
@@ -349,6 +349,8 @@ vi.mock('antd', () => {
|
||||
</button>
|
||||
);
|
||||
Button.Group = ({ children }: any) => <div>{children}</div>;
|
||||
const Space: any = ({ children }: any) => <div>{children}</div>;
|
||||
Space.Compact = ({ children, className }: any) => <div className={className}>{children}</div>;
|
||||
|
||||
const Form: any = ({ children }: any) => <form>{children}</form>;
|
||||
Form.Item = ({ children }: any) => <>{children}</>;
|
||||
@@ -356,6 +358,7 @@ vi.mock('antd', () => {
|
||||
|
||||
return {
|
||||
Button,
|
||||
Space,
|
||||
message: messageApi,
|
||||
Modal: ({ children, open, onOk, okText = '确认' }: any) => (open ? (
|
||||
<section>
|
||||
@@ -3707,16 +3710,18 @@ describe('QueryEditor external SQL save', () => {
|
||||
|
||||
it('keeps the v2 query editor toolbar grouped and compact', () => {
|
||||
const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
|
||||
const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');
|
||||
const transactionSettingsSource = readFileSync(new URL('./QueryEditorTransactionSettings.tsx', import.meta.url), 'utf8');
|
||||
const transactionToolbarSource = readFileSync(new URL('./QueryEditorTransactionToolbar.tsx', import.meta.url), 'utf8');
|
||||
const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8');
|
||||
|
||||
expect(source).toContain('gn-v2-query-toolbar-selects');
|
||||
expect(source).toContain('gn-v2-query-toolbar-actions');
|
||||
expect(source).toContain('gn-v2-query-toolbar-connection-select');
|
||||
expect(source).toContain('gn-v2-query-toolbar-database-select');
|
||||
expect(source).toContain('gn-v2-query-toolbar-max-rows-select');
|
||||
expect(source).toContain('QueryEditorTransactionSettings');
|
||||
expect(source).toContain('QueryEditorToolbar');
|
||||
expect(toolbarSource).toContain('gn-v2-query-toolbar-selects');
|
||||
expect(toolbarSource).toContain('gn-v2-query-toolbar-actions');
|
||||
expect(toolbarSource).toContain('gn-v2-query-toolbar-connection-select');
|
||||
expect(toolbarSource).toContain('gn-v2-query-toolbar-database-select');
|
||||
expect(toolbarSource).toContain('gn-v2-query-toolbar-max-rows-select');
|
||||
expect(toolbarSource).toContain('QueryEditorTransactionSettings');
|
||||
expect(transactionSettingsSource).toContain('gn-v2-query-toolbar-transaction-mode-select');
|
||||
expect(transactionSettingsSource).toContain('gn-v2-query-toolbar-transaction-delay-select');
|
||||
expect(transactionSettingsSource).toContain('参考 DBeaver');
|
||||
@@ -3729,10 +3734,10 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(transactionToolbarSource).toContain('未提交 ${statementCount} 条变更语句');
|
||||
expect(transactionToolbarSource).toContain('事务执行成功${pendingCountText},正在自动提交');
|
||||
expect(transactionToolbarSource).toContain('onFinish');
|
||||
expect(source).toContain('gn-v2-query-toolbar-action-group');
|
||||
expect(toolbarSource).toContain('gn-v2-query-toolbar-action-group');
|
||||
expect(transactionSettingsSource).toContain('style={isV2Ui ? undefined : { width: 160 }}');
|
||||
expect(source).toContain('style={isV2Ui ? undefined : { width: 200 }}');
|
||||
expect(source).toContain('style={isV2Ui ? undefined : { width: 170 }}');
|
||||
expect(toolbarSource).toContain('style={isV2Ui ? undefined : { width: 200 }}');
|
||||
expect(toolbarSource).toContain('style={isV2Ui ? undefined : { width: 170 }}');
|
||||
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-selects');
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-actions');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 } from 'antd';
|
||||
import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined, StopOutlined, RobotOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
import { message, Modal, Input, Form, MenuProps } from 'antd';
|
||||
import { format } from 'sql-formatter';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { TabData, ColumnDefinition, IndexDefinition } from '../types';
|
||||
@@ -41,10 +40,9 @@ import {
|
||||
getColumnDefinitionName,
|
||||
} from '../utils/columnDefinition';
|
||||
import QueryEditorResultsPanel, { type QueryEditorResultSet } from './QueryEditorResultsPanel';
|
||||
import QueryEditorTransactionSettings, {
|
||||
SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS,
|
||||
} from './QueryEditorTransactionSettings';
|
||||
import { SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS } from './QueryEditorTransactionSettings';
|
||||
import QueryEditorTransactionToolbar from './QueryEditorTransactionToolbar';
|
||||
import QueryEditorToolbar from './QueryEditorToolbar';
|
||||
import { useSqlEditorTransactionController } from './useSqlEditorTransactionController';
|
||||
|
||||
// HMR 重载时释放旧注册避免补全和 hover 内容重复
|
||||
@@ -5134,134 +5132,44 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
className={isV2Ui ? 'gn-v2-query-editor-pane' : undefined}
|
||||
style={{ display: 'flex', flexDirection: 'column', minHeight: 0, flex: isResultPanelVisible ? '0 0 auto' : '1 1 auto' }}
|
||||
>
|
||||
<div className={isV2Ui ? 'gn-v2-query-toolbar' : undefined} style={{ padding: '4px 8px 8px', display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}>
|
||||
<div
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-selects' : undefined}
|
||||
style={{ display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}
|
||||
>
|
||||
<Select
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-connection-select' : undefined}
|
||||
style={isV2Ui ? undefined : { width: 150 }}
|
||||
placeholder="选择连接"
|
||||
value={currentConnectionId}
|
||||
onChange={(val) => {
|
||||
setCurrentConnectionId(val);
|
||||
setCurrentDb('');
|
||||
}}
|
||||
options={queryCapableConnections.map(c => ({ label: c.name, value: c.id }))}
|
||||
showSearch
|
||||
/>
|
||||
<Select
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-database-select' : undefined}
|
||||
style={isV2Ui ? undefined : { width: 200 }}
|
||||
placeholder="选择数据库"
|
||||
value={currentDb}
|
||||
onChange={setCurrentDb}
|
||||
options={dbList.map(db => ({ label: db, value: db }))}
|
||||
showSearch
|
||||
/>
|
||||
<Tooltip title="最大返回行数(会对 SELECT 自动加 LIMIT,防止大结果集卡死)">
|
||||
<Select
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-max-rows-select' : undefined}
|
||||
style={isV2Ui ? undefined : { width: 170 }}
|
||||
value={queryOptions?.maxRows ?? 5000}
|
||||
onChange={(val) => setQueryOptions({ maxRows: Number(val) })}
|
||||
options={[
|
||||
{ label: '最大行数:500', value: 500 },
|
||||
{ label: '最大行数:1000', value: 1000 },
|
||||
{ label: '最大行数:5000', value: 5000 },
|
||||
{ label: '最大行数:20000', value: 20000 },
|
||||
{ label: '最大行数:不限', value: 0 },
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
<QueryEditorTransactionSettings
|
||||
isV2Ui={isV2Ui}
|
||||
commitMode={sqlEditorCommitMode}
|
||||
autoCommitDelayMs={sqlEditorAutoCommitDelayMs}
|
||||
onCommitModeChange={(mode) => setSqlEditorTransactionOptions(
|
||||
mode === 'auto'
|
||||
? { commitMode: mode, autoCommitDelayMs: 0 }
|
||||
: { commitMode: mode },
|
||||
)}
|
||||
onAutoCommitDelayMsChange={(delayMs) => setSqlEditorTransactionOptions({ autoCommitDelayMs: delayMs })}
|
||||
/>
|
||||
{pendingSqlTransaction && sqlEditorTransactionToolbar}
|
||||
</div>
|
||||
<div
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-actions' : undefined}
|
||||
style={{ display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}
|
||||
>
|
||||
<Button.Group className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
|
||||
<Tooltip
|
||||
title={
|
||||
runQueryShortcutBinding.enabled && runQueryShortcutBinding.combo
|
||||
? `运行(${getShortcutDisplayLabel(runQueryShortcutBinding.combo, activeShortcutPlatform)})`
|
||||
: '运行'
|
||||
}
|
||||
>
|
||||
<Button className={isV2Ui ? 'gn-v2-query-toolbar-run-action' : undefined} type="primary" icon={<PlayCircleOutlined />} onMouseDown={captureEditorCursorPosition} onClick={handleRun} loading={loading}>
|
||||
运行
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{loading && (
|
||||
<Button type="primary" danger icon={<StopOutlined />} onClick={handleCancel}>
|
||||
停止
|
||||
</Button>
|
||||
)}
|
||||
</Button.Group>
|
||||
<Button.Group className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
|
||||
<Tooltip
|
||||
title={
|
||||
saveQueryShortcutBinding.enabled && saveQueryShortcutBinding.combo
|
||||
? `保存(${getShortcutDisplayLabel(saveQueryShortcutBinding.combo, activeShortcutPlatform)})`
|
||||
: '保存'
|
||||
}
|
||||
>
|
||||
<Button icon={<SaveOutlined />} onClick={handleQuickSave}>
|
||||
保存
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Dropdown menu={{ items: saveMoreMenuItems }} placement="bottomRight">
|
||||
<Button>更多</Button>
|
||||
</Dropdown>
|
||||
</Button.Group>
|
||||
|
||||
<Button.Group className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
|
||||
<Tooltip title="美化 SQL">
|
||||
<Button icon={<FormatPainterOutlined />} onClick={handleFormat}>美化</Button>
|
||||
</Tooltip>
|
||||
<Dropdown menu={{ items: formatSettingsMenu }} placement="bottomRight">
|
||||
<Button className={isV2Ui ? 'gn-v2-query-toolbar-icon-action' : undefined} icon={<SettingOutlined />} />
|
||||
</Dropdown>
|
||||
</Button.Group>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
toggleQueryResultsPanelShortcutBinding.enabled && toggleQueryResultsPanelShortcutBinding.combo
|
||||
? `${isResultPanelVisible ? '隐藏结果区' : '显示结果区'}(${getShortcutDisplayLabel(toggleQueryResultsPanelShortcutBinding.combo, activeShortcutPlatform)})`
|
||||
: (isResultPanelVisible ? '隐藏结果区' : '显示结果区')
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon={isResultPanelVisible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={toggleResultPanelVisibility}
|
||||
>
|
||||
结果
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Dropdown menu={{ items: [
|
||||
{ key: 'ai-generate', label: '生成 SQL', icon: <RobotOutlined />, onClick: () => handleAIAction('generate') },
|
||||
{ key: 'ai-explain', label: '解释 SQL', icon: <RobotOutlined />, onClick: () => handleAIAction('explain') },
|
||||
{ key: 'ai-optimize', label: '优化 SQL', icon: <RobotOutlined />, onClick: () => handleAIAction('optimize') },
|
||||
{ type: 'divider' as const },
|
||||
{ key: 'ai-schema', label: 'Schema 分析', icon: <RobotOutlined />, onClick: () => handleAIAction('schema') },
|
||||
] }} placement="bottomRight">
|
||||
<Button className={isV2Ui ? 'gn-v2-query-toolbar-ai-action' : undefined} icon={<RobotOutlined />} style={{ color: '#818cf8' }}>AI</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<QueryEditorToolbar
|
||||
isV2Ui={isV2Ui}
|
||||
currentConnectionId={currentConnectionId}
|
||||
currentDb={currentDb}
|
||||
queryCapableConnections={queryCapableConnections}
|
||||
dbList={dbList}
|
||||
maxRows={queryOptions?.maxRows ?? 5000}
|
||||
sqlEditorCommitMode={sqlEditorCommitMode}
|
||||
sqlEditorAutoCommitDelayMs={sqlEditorAutoCommitDelayMs}
|
||||
pendingTransactionToolbar={pendingSqlTransaction ? sqlEditorTransactionToolbar : null}
|
||||
runQueryShortcutBinding={runQueryShortcutBinding}
|
||||
saveQueryShortcutBinding={saveQueryShortcutBinding}
|
||||
toggleQueryResultsPanelShortcutBinding={toggleQueryResultsPanelShortcutBinding}
|
||||
activeShortcutPlatform={activeShortcutPlatform}
|
||||
isResultPanelVisible={isResultPanelVisible}
|
||||
loading={loading}
|
||||
saveMoreMenuItems={saveMoreMenuItems}
|
||||
formatSettingsMenu={formatSettingsMenu}
|
||||
onConnectionChange={(val) => {
|
||||
setCurrentConnectionId(val);
|
||||
setCurrentDb('');
|
||||
}}
|
||||
onDatabaseChange={setCurrentDb}
|
||||
onMaxRowsChange={(maxRows) => setQueryOptions({ maxRows })}
|
||||
onCommitModeChange={(mode) => setSqlEditorTransactionOptions(
|
||||
mode === 'auto'
|
||||
? { commitMode: mode, autoCommitDelayMs: 0 }
|
||||
: { commitMode: mode },
|
||||
)}
|
||||
onAutoCommitDelayMsChange={(delayMs) => setSqlEditorTransactionOptions({ autoCommitDelayMs: delayMs })}
|
||||
onCaptureEditorCursorPosition={captureEditorCursorPosition}
|
||||
onRun={handleRun}
|
||||
onCancel={handleCancel}
|
||||
onQuickSave={handleQuickSave}
|
||||
onFormat={handleFormat}
|
||||
onToggleResultPanelVisibility={toggleResultPanelVisibility}
|
||||
onAIAction={handleAIAction}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={editorShellRef}
|
||||
|
||||
204
frontend/src/components/QueryEditorToolbar.tsx
Normal file
204
frontend/src/components/QueryEditorToolbar.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import React from 'react';
|
||||
import { Button, Dropdown, Select, Space, Tooltip, type MenuProps } from 'antd';
|
||||
import {
|
||||
EyeInvisibleOutlined,
|
||||
EyeOutlined,
|
||||
FormatPainterOutlined,
|
||||
PlayCircleOutlined,
|
||||
RobotOutlined,
|
||||
SaveOutlined,
|
||||
SettingOutlined,
|
||||
StopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import type { SavedConnection } from '../types';
|
||||
import { getShortcutDisplayLabel, type ShortcutPlatform, type ShortcutPlatformBinding } from '../utils/shortcuts';
|
||||
import QueryEditorTransactionSettings, { type SqlEditorCommitMode } from './QueryEditorTransactionSettings';
|
||||
|
||||
type QueryEditorToolbarProps = {
|
||||
isV2Ui: boolean;
|
||||
currentConnectionId: string;
|
||||
currentDb: string;
|
||||
queryCapableConnections: SavedConnection[];
|
||||
dbList: string[];
|
||||
maxRows: number;
|
||||
sqlEditorCommitMode: SqlEditorCommitMode;
|
||||
sqlEditorAutoCommitDelayMs: number;
|
||||
pendingTransactionToolbar: React.ReactNode;
|
||||
runQueryShortcutBinding: ShortcutPlatformBinding;
|
||||
saveQueryShortcutBinding: ShortcutPlatformBinding;
|
||||
toggleQueryResultsPanelShortcutBinding: ShortcutPlatformBinding;
|
||||
activeShortcutPlatform: ShortcutPlatform;
|
||||
isResultPanelVisible: boolean;
|
||||
loading: boolean;
|
||||
saveMoreMenuItems: MenuProps['items'];
|
||||
formatSettingsMenu: MenuProps['items'];
|
||||
onConnectionChange: (connectionId: string) => void;
|
||||
onDatabaseChange: (dbName: string) => void;
|
||||
onMaxRowsChange: (maxRows: number) => void;
|
||||
onCommitModeChange: (mode: SqlEditorCommitMode) => void;
|
||||
onAutoCommitDelayMsChange: (delayMs: number) => void;
|
||||
onCaptureEditorCursorPosition: () => void;
|
||||
onRun: () => void;
|
||||
onCancel: () => void;
|
||||
onQuickSave: () => void;
|
||||
onFormat: () => void;
|
||||
onToggleResultPanelVisibility: () => void;
|
||||
onAIAction: (action: 'generate' | 'explain' | 'optimize' | 'schema') => void;
|
||||
};
|
||||
|
||||
const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
isV2Ui,
|
||||
currentConnectionId,
|
||||
currentDb,
|
||||
queryCapableConnections,
|
||||
dbList,
|
||||
maxRows,
|
||||
sqlEditorCommitMode,
|
||||
sqlEditorAutoCommitDelayMs,
|
||||
pendingTransactionToolbar,
|
||||
runQueryShortcutBinding,
|
||||
saveQueryShortcutBinding,
|
||||
toggleQueryResultsPanelShortcutBinding,
|
||||
activeShortcutPlatform,
|
||||
isResultPanelVisible,
|
||||
loading,
|
||||
saveMoreMenuItems,
|
||||
formatSettingsMenu,
|
||||
onConnectionChange,
|
||||
onDatabaseChange,
|
||||
onMaxRowsChange,
|
||||
onCommitModeChange,
|
||||
onAutoCommitDelayMsChange,
|
||||
onCaptureEditorCursorPosition,
|
||||
onRun,
|
||||
onCancel,
|
||||
onQuickSave,
|
||||
onFormat,
|
||||
onToggleResultPanelVisibility,
|
||||
onAIAction,
|
||||
}) => (
|
||||
<div className={isV2Ui ? 'gn-v2-query-toolbar' : undefined} style={{ padding: '4px 8px 8px', display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}>
|
||||
<div
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-selects' : undefined}
|
||||
style={{ display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}
|
||||
>
|
||||
<Select
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-connection-select' : undefined}
|
||||
style={isV2Ui ? undefined : { width: 150 }}
|
||||
placeholder="选择连接"
|
||||
value={currentConnectionId}
|
||||
onChange={onConnectionChange}
|
||||
options={queryCapableConnections.map(c => ({ label: c.name, value: c.id }))}
|
||||
showSearch
|
||||
/>
|
||||
<Select
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-database-select' : undefined}
|
||||
style={isV2Ui ? undefined : { width: 200 }}
|
||||
placeholder="选择数据库"
|
||||
value={currentDb}
|
||||
onChange={onDatabaseChange}
|
||||
options={dbList.map(db => ({ label: db, value: db }))}
|
||||
showSearch
|
||||
/>
|
||||
<Tooltip title="最大返回行数(会对 SELECT 自动加 LIMIT,防止大结果集卡死)">
|
||||
<Select
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-max-rows-select' : undefined}
|
||||
style={isV2Ui ? undefined : { width: 170 }}
|
||||
value={maxRows}
|
||||
onChange={(val) => onMaxRowsChange(Number(val))}
|
||||
options={[
|
||||
{ label: '最大行数:500', value: 500 },
|
||||
{ label: '最大行数:1000', value: 1000 },
|
||||
{ label: '最大行数:5000', value: 5000 },
|
||||
{ label: '最大行数:20000', value: 20000 },
|
||||
{ label: '最大行数:不限', value: 0 },
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
<QueryEditorTransactionSettings
|
||||
isV2Ui={isV2Ui}
|
||||
commitMode={sqlEditorCommitMode}
|
||||
autoCommitDelayMs={sqlEditorAutoCommitDelayMs}
|
||||
onCommitModeChange={onCommitModeChange}
|
||||
onAutoCommitDelayMsChange={onAutoCommitDelayMsChange}
|
||||
/>
|
||||
{pendingTransactionToolbar}
|
||||
</div>
|
||||
<div
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-actions' : undefined}
|
||||
style={{ display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}
|
||||
>
|
||||
<Space.Compact className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
|
||||
<Tooltip
|
||||
title={
|
||||
runQueryShortcutBinding.enabled && runQueryShortcutBinding.combo
|
||||
? `运行(${getShortcutDisplayLabel(runQueryShortcutBinding.combo, activeShortcutPlatform)})`
|
||||
: '运行'
|
||||
}
|
||||
>
|
||||
<Button className={isV2Ui ? 'gn-v2-query-toolbar-run-action' : undefined} type="primary" icon={<PlayCircleOutlined />} onMouseDown={onCaptureEditorCursorPosition} onClick={onRun} loading={loading}>
|
||||
运行
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{loading && (
|
||||
<Button type="primary" danger icon={<StopOutlined />} onClick={onCancel}>
|
||||
停止
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
<Space.Compact className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
|
||||
<Tooltip
|
||||
title={
|
||||
saveQueryShortcutBinding.enabled && saveQueryShortcutBinding.combo
|
||||
? `保存(${getShortcutDisplayLabel(saveQueryShortcutBinding.combo, activeShortcutPlatform)})`
|
||||
: '保存'
|
||||
}
|
||||
>
|
||||
<Button icon={<SaveOutlined />} onClick={onQuickSave}>
|
||||
保存
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Dropdown menu={{ items: saveMoreMenuItems }} placement="bottomRight">
|
||||
<Button>更多</Button>
|
||||
</Dropdown>
|
||||
</Space.Compact>
|
||||
|
||||
<Space.Compact className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
|
||||
<Tooltip title="美化 SQL">
|
||||
<Button icon={<FormatPainterOutlined />} onClick={onFormat}>美化</Button>
|
||||
</Tooltip>
|
||||
<Dropdown menu={{ items: formatSettingsMenu }} placement="bottomRight">
|
||||
<Button className={isV2Ui ? 'gn-v2-query-toolbar-icon-action' : undefined} icon={<SettingOutlined />} />
|
||||
</Dropdown>
|
||||
</Space.Compact>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
toggleQueryResultsPanelShortcutBinding.enabled && toggleQueryResultsPanelShortcutBinding.combo
|
||||
? `${isResultPanelVisible ? '隐藏结果区' : '显示结果区'}(${getShortcutDisplayLabel(toggleQueryResultsPanelShortcutBinding.combo, activeShortcutPlatform)})`
|
||||
: (isResultPanelVisible ? '隐藏结果区' : '显示结果区')
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon={isResultPanelVisible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={onToggleResultPanelVisibility}
|
||||
>
|
||||
结果
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Dropdown menu={{ items: [
|
||||
{ key: 'ai-generate', label: '生成 SQL', icon: <RobotOutlined />, onClick: () => onAIAction('generate') },
|
||||
{ key: 'ai-explain', label: '解释 SQL', icon: <RobotOutlined />, onClick: () => onAIAction('explain') },
|
||||
{ key: 'ai-optimize', label: '优化 SQL', icon: <RobotOutlined />, onClick: () => onAIAction('optimize') },
|
||||
{ type: 'divider' as const },
|
||||
{ key: 'ai-schema', label: 'Schema 分析', icon: <RobotOutlined />, onClick: () => onAIAction('schema') },
|
||||
] }} placement="bottomRight">
|
||||
<Button className={isV2Ui ? 'gn-v2-query-toolbar-ai-action' : undefined} icon={<RobotOutlined />} style={{ color: '#818cf8' }}>AI</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default QueryEditorToolbar;
|
||||
@@ -3,9 +3,13 @@ export interface CodebaseHotspotEntry {
|
||||
lines: number;
|
||||
area: string;
|
||||
riskLevel: 'medium' | 'high' | 'critical';
|
||||
readiness: 'readyToExtract' | 'needsCharacterizationTests';
|
||||
why: string;
|
||||
preferredNextSlice: string;
|
||||
safeSeam: string;
|
||||
suggestedSlices: string[];
|
||||
testTargets: string[];
|
||||
verificationPlan: string[];
|
||||
}
|
||||
|
||||
export interface CodebaseHotspotSnapshotOptions {
|
||||
@@ -18,84 +22,156 @@ export interface CodebaseHotspotSnapshotOptions {
|
||||
const CODEBASE_HOTSPOT_SNAPSHOT: CodebaseHotspotEntry[] = [
|
||||
{
|
||||
path: 'frontend/src/components/Sidebar.tsx',
|
||||
lines: 8910,
|
||||
lines: 8901,
|
||||
area: 'workspace-navigation',
|
||||
riskLevel: 'critical',
|
||||
readiness: 'needsCharacterizationTests',
|
||||
why: '左侧树、命令面板、上下文菜单和连接动作集中在单文件,修改入口多且回归面大。',
|
||||
preferredNextSlice: '外部 SQL 目录弹窗',
|
||||
safeSeam: '优先抽出无状态弹窗/菜单配置,再处理依赖连接树状态的动作分发。',
|
||||
suggestedSlices: ['V2 命令面板', '外部 SQL 目录弹窗', '连接树动作', '批量操作弹窗'],
|
||||
testTargets: ['Sidebar.locate-toolbar.test.tsx', 'sidebarV2Utils.test.ts'],
|
||||
verificationPlan: [
|
||||
'npm --prefix frontend test -- Sidebar.locate-toolbar.test.tsx sidebarV2Utils.test.ts',
|
||||
'npm --prefix frontend run build',
|
||||
'浏览器打开侧边栏,验证连接树、右键菜单和外部 SQL 目录入口可用。',
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'frontend/src/components/DataGrid.tsx',
|
||||
lines: 8080,
|
||||
area: 'result-grid',
|
||||
riskLevel: 'critical',
|
||||
readiness: 'needsCharacterizationTests',
|
||||
why: '结果展示、编辑、DDL、导出和列操作耦合,容易让单点修复影响查询结果区。',
|
||||
preferredNextSlice: '结果导出工具栏',
|
||||
safeSeam: '优先抽出纯展示工具栏和菜单项生成,不先移动数据编辑事务状态。',
|
||||
suggestedSlices: ['结果导出工具栏', '列头菜单', 'DDL 视图', '单元格编辑事务提示'],
|
||||
testTargets: ['DataGrid.layout.test.tsx', 'DataGrid.ddl.test.tsx'],
|
||||
verificationPlan: [
|
||||
'npm --prefix frontend test -- DataGrid.layout.test.tsx DataGrid.ddl.test.tsx',
|
||||
'npm --prefix frontend run build',
|
||||
'浏览器执行查询并验证结果表、导出、列菜单和表格编辑入口。',
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'frontend/src/components/ConnectionModal.tsx',
|
||||
lines: 7462,
|
||||
lines: 6811,
|
||||
area: 'connection-form',
|
||||
riskLevel: 'critical',
|
||||
readiness: 'readyToExtract',
|
||||
why: '多数据源连接表单仍集中在一个组件,新增数据源或密钥规则时容易互相影响。',
|
||||
preferredNextSlice: 'TLS 配置区',
|
||||
safeSeam: '连接表单已有 presentation utils,可先抽出按数据源显示的配置分区组件。',
|
||||
suggestedSlices: ['SSH/代理配置区', 'TLS 配置区', 'MongoDB 配置区', 'JVM 配置区'],
|
||||
testTargets: ['ConnectionModal.edit-password.test.tsx', 'connectionModalPresentation.test.ts'],
|
||||
verificationPlan: [
|
||||
'npm --prefix frontend test -- ConnectionModal.edit-password.test.tsx connectionModalPresentation.test.ts',
|
||||
'npm --prefix frontend run build',
|
||||
'浏览器打开新增/编辑连接弹窗,切换 MySQL、Oracle、MongoDB、Redis 表单。',
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'frontend/src/components/QueryEditor.tsx',
|
||||
lines: 5367,
|
||||
lines: 5275,
|
||||
area: 'sql-editor',
|
||||
riskLevel: 'critical',
|
||||
readiness: 'readyToExtract',
|
||||
why: 'SQL 编辑、执行、事务、结果区布局和快捷键状态集中,事务和结果区回归风险高。',
|
||||
preferredNextSlice: '编辑器工具栏',
|
||||
safeSeam: '工具栏 JSX 可通过 props 透传状态和回调,避免触碰 SQL 执行、事务和结果分页逻辑。',
|
||||
suggestedSlices: ['结果区工具栏', '事务状态条', '执行日志提示', '编辑器快捷键绑定'],
|
||||
testTargets: ['QueryEditor.result-panel.test.tsx', 'useSqlEditorTransactionController.test.ts'],
|
||||
verificationPlan: [
|
||||
'npm --prefix frontend test -- QueryEditor.external-sql-save.test.tsx useSqlEditorTransactionController.test.tsx',
|
||||
'npm --prefix frontend run build',
|
||||
'浏览器打开 SQL 编辑器,验证连接/库选择、运行、保存、美化、结果区显隐和 AI 菜单。',
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'frontend/src/components/TableDesigner.tsx',
|
||||
lines: 3549,
|
||||
area: 'table-designer',
|
||||
riskLevel: 'high',
|
||||
readiness: 'needsCharacterizationTests',
|
||||
why: '字段编辑、索引、外键、分区和 DDL 生成集中,数据库方言差异容易扩散。',
|
||||
preferredNextSlice: '字段编辑表格',
|
||||
safeSeam: '先补字段类型/长度/NULL/默认值快照测试,再抽字段编辑表格。',
|
||||
suggestedSlices: ['字段编辑表格', '索引配置面板', '外键配置面板', '方言 DDL 预览'],
|
||||
testTargets: ['TableDesigner.*.test.tsx', 'tableDesignerSchemaSql.test.ts'],
|
||||
verificationPlan: [
|
||||
'npm --prefix frontend test -- tableDesignerSchemaSql.test.ts',
|
||||
'npm --prefix frontend run build',
|
||||
'浏览器打开对象设计,验证字段、索引、外键和 DDL 预览。',
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'frontend/src/components/RedisViewer.tsx',
|
||||
lines: 2120,
|
||||
area: 'redis-browser',
|
||||
riskLevel: 'high',
|
||||
readiness: 'readyToExtract',
|
||||
why: 'Key 浏览、不同数据结构编辑、TTL、编码显示和新增弹窗集中,Redis Cluster/Sentinel 后续验证面较宽。',
|
||||
preferredNextSlice: 'Key 搜索栏',
|
||||
safeSeam: '先抽出搜索栏和拓扑提示,避免提前改动各数据结构编辑器。',
|
||||
suggestedSlices: ['Key 搜索栏', 'String/List/Set/ZSet/Hash/Stream 编辑器', '新增 Key 弹窗'],
|
||||
testTargets: ['redisViewerTree.test.ts', 'RedisViewer.*.test.tsx'],
|
||||
verificationPlan: [
|
||||
'npm --prefix frontend test -- redisViewerTree.test.ts',
|
||||
'npm --prefix frontend run build',
|
||||
'浏览器打开 Redis 连接,验证 key 搜索、刷新、TTL 和新增入口。',
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'frontend/src/components/DriverManagerModal.tsx',
|
||||
lines: 1729,
|
||||
area: 'driver-manager',
|
||||
riskLevel: 'high',
|
||||
readiness: 'readyToExtract',
|
||||
why: '驱动安装、状态展示、下载和可选代理逻辑较多,适合继续拆出状态卡片和操作区。',
|
||||
preferredNextSlice: '驱动状态列表',
|
||||
safeSeam: '状态列表是展示型组件,可先抽出再保留安装/下载动作在父组件。',
|
||||
suggestedSlices: ['驱动状态列表', '安装操作区', '下载日志区'],
|
||||
testTargets: ['DriverManagerModal.*.test.tsx'],
|
||||
verificationPlan: [
|
||||
'npm --prefix frontend test -- DriverManagerModal.*.test.tsx',
|
||||
'npm --prefix frontend run build',
|
||||
'浏览器打开驱动管理,验证状态展示、安装按钮和日志区域。',
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'frontend/src/components/DataSyncModal.tsx',
|
||||
lines: 1526,
|
||||
area: 'data-sync',
|
||||
riskLevel: 'high',
|
||||
readiness: 'needsCharacterizationTests',
|
||||
why: '数据同步连接、表映射、预检查和执行结果集中,数据库方言问题容易隐藏。',
|
||||
preferredNextSlice: '同步预检查结果',
|
||||
safeSeam: '先把预检查结果做成纯展示组件,保留连接选择和执行动作在父组件。',
|
||||
suggestedSlices: ['连接选择区', '表映射区', '同步预检查结果', '执行日志区'],
|
||||
testTargets: ['DataSyncModal.*.test.tsx'],
|
||||
verificationPlan: [
|
||||
'npm --prefix frontend test -- DataSyncModal.*.test.tsx',
|
||||
'npm --prefix frontend run build',
|
||||
'浏览器打开数据同步,验证连接选择、表映射、预检查和执行日志。',
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'frontend/src/components/JVMDiagnosticConsole.tsx',
|
||||
lines: 1146,
|
||||
area: 'jvm-diagnostics',
|
||||
riskLevel: 'medium',
|
||||
readiness: 'readyToExtract',
|
||||
why: '诊断命令、输出块、权限提示和会话状态可继续拆分,降低 JVM 诊断回归面。',
|
||||
preferredNextSlice: '诊断输出区',
|
||||
safeSeam: '输出区主要依赖命令结果数组,可先抽成展示组件。',
|
||||
suggestedSlices: ['命令输入区', '诊断输出区', '权限提示区'],
|
||||
testTargets: ['JVMDiagnosticConsole.*.test.tsx'],
|
||||
verificationPlan: [
|
||||
'npm --prefix frontend test -- JVMDiagnosticConsole.*.test.tsx',
|
||||
'npm --prefix frontend run build',
|
||||
'浏览器打开 JVM 诊断面板,验证命令输入、输出和权限提示。',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -144,7 +220,7 @@ export const buildCodebaseHotspotSnapshot = ({
|
||||
source: 'static_maintainability_snapshot',
|
||||
evidence: {
|
||||
measuredAt: '2026-06-12',
|
||||
note: '基于当前仓库前端文件行数热点快照;提交前仍应用 git diff 和定向测试核对具体改动。',
|
||||
note: '基于当前仓库前端文件行数热点快照;拆分前应优先选择 readyToExtract 且已有测试覆盖的 slice。',
|
||||
},
|
||||
filters: {
|
||||
keyword: normalizedKeyword || undefined,
|
||||
@@ -165,13 +241,18 @@ export const buildCodebaseHotspotSnapshot = ({
|
||||
lines: entry.lines,
|
||||
area: entry.area,
|
||||
riskLevel: entry.riskLevel,
|
||||
readiness: entry.readiness,
|
||||
why: entry.why,
|
||||
preferredNextSlice: includeRecommendations ? entry.preferredNextSlice : undefined,
|
||||
safeSeam: includeRecommendations ? entry.safeSeam : undefined,
|
||||
suggestedSlices: includeRecommendations ? entry.suggestedSlices : [],
|
||||
testTargets: includeRecommendations ? entry.testTargets : [],
|
||||
verificationPlan: includeRecommendations ? entry.verificationPlan : [],
|
||||
})),
|
||||
nextActions: includeRecommendations
|
||||
? [
|
||||
'优先选择已有测试覆盖的 slice 做小步拆分,避免直接重写整个大组件。',
|
||||
'优先选择 readiness=readyToExtract 且已有测试覆盖的 slice 做小步拆分,避免直接重写整个大组件。',
|
||||
'每次拆分都要先确认 safeSeam,不跨越 SQL 执行、事务、连接密钥或数据库方言边界。',
|
||||
'拆分后至少运行对应组件测试、相关 utils 测试和 npm --prefix frontend run build。',
|
||||
'涉及可见 UI 的拆分需要用浏览器打开真实页面做一次冒烟验证。',
|
||||
]
|
||||
|
||||
@@ -45,8 +45,13 @@ describe('aiLocalToolExecutor inspect_codebase_hotspots', () => {
|
||||
expect(result.content).toContain('"kind":"codebase_hotspots"');
|
||||
expect(result.content).toContain('frontend/src/components/QueryEditor.tsx');
|
||||
expect(result.content).toContain('"riskLevel":"critical"');
|
||||
expect(result.content).toContain('"readiness":"readyToExtract"');
|
||||
expect(result.content).toContain('"preferredNextSlice":"编辑器工具栏"');
|
||||
expect(result.content).toContain('工具栏 JSX 可通过 props 透传状态和回调');
|
||||
expect(result.content).toContain('事务状态条');
|
||||
expect(result.content).toContain('QueryEditor.result-panel.test.tsx');
|
||||
expect(result.content).toContain('QueryEditor.external-sql-save.test.tsx');
|
||||
expect(result.content).toContain('浏览器打开 SQL 编辑器');
|
||||
expect(result.content).not.toContain('import React');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,14 +168,14 @@ export const BUILTIN_AI_INSPECTION_DIAGNOSTICS_TOOL_INFO: AIBuiltinToolInfo[] =
|
||||
icon: "🧱",
|
||||
desc: "查看前端大文件和拆分热点",
|
||||
detail:
|
||||
"返回当前 GoNavi 前端代码中的大文件热点、行数、风险等级、建议拆分切片和应该运行的回归测试。适合用户要求继续治理几千行大文件、评估下一步该拆哪个组件,或 AI 在修改前需要先判断改动风险时调用。",
|
||||
"返回当前 GoNavi 前端代码中的大文件热点、行数、风险等级、拆分成熟度、安全边界、建议拆分切片和应该运行的回归测试。适合用户要求继续治理几千行大文件、评估下一步该拆哪个组件,或 AI 在修改前需要先判断改动风险时调用。",
|
||||
params: "keyword?, minLines?(默认 1000), limit?(默认 8), includeRecommendations?(默认 true)",
|
||||
tool: {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "inspect_codebase_hotspots",
|
||||
description:
|
||||
"读取 GoNavi 前端大文件和拆分热点快照,返回文件路径、行数、风险等级、建议拆分切片和测试目标。适用于用户提到几千行文件太臃肿、需要继续拆分组件、评估下一个重构切入点或在改 UI/AI/MCP 前需要先判断代码热点风险时优先调用。",
|
||||
"读取 GoNavi 前端大文件和拆分热点快照,返回文件路径、行数、风险等级、拆分成熟度、首选切片、安全拆分边界、建议拆分切片、测试目标和验证计划。适用于用户提到几千行文件太臃肿、需要继续拆分组件、评估下一个重构切入点或在改 UI/AI/MCP 前需要先判断代码热点风险时优先调用。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
||||
Reference in New Issue
Block a user