mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-22 21:27:11 +08:00
- 新增独立内联补全模型配置与服务端透传 - 优化 SQL 编辑器 Alt+\\ 触发、ghost 延续与对象位补全 - 引入基于已存查询和执行日志的 SQL 记忆补全 - 补充前后端本地化、快捷键与补全回归测试
501 lines
15 KiB
TypeScript
501 lines
15 KiB
TypeScript
import React from "react";
|
|
import { Button, Dropdown, Select, Tooltip, type MenuProps } from "antd";
|
|
import {
|
|
DownOutlined,
|
|
EyeInvisibleOutlined,
|
|
EyeOutlined,
|
|
FormatPainterOutlined,
|
|
PlayCircleOutlined,
|
|
RobotOutlined,
|
|
SearchOutlined,
|
|
SaveOutlined,
|
|
SettingOutlined,
|
|
StopOutlined,
|
|
} from "@ant-design/icons";
|
|
|
|
import { t as defaultTranslate } from '../i18n';
|
|
import { useOptionalI18n } from '../i18n/provider';
|
|
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;
|
|
formatSqlShortcutBinding: ShortcutPlatformBinding;
|
|
triggerSqlAiCompletionShortcutBinding: 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;
|
|
onFindInEditor: () => void;
|
|
onFormat: () => void;
|
|
onTriggerSqlAiCompletion: () => void;
|
|
onToggleResultPanelVisibility: () => void;
|
|
onAIAction: (action: "generate" | "explain" | "optimize" | "schema") => void;
|
|
};
|
|
|
|
const FULL_NAME_TOOLTIP_DELAY_SECONDS = 1;
|
|
|
|
type FullNameSelectOption = {
|
|
label: string;
|
|
value: string;
|
|
title: string;
|
|
fullName: string;
|
|
};
|
|
|
|
const renderFullNameSelectTooltip = (fullName: React.ReactNode) => {
|
|
const fullNameText = String(fullName ?? "");
|
|
|
|
return (
|
|
<Tooltip
|
|
title={fullNameText}
|
|
mouseEnterDelay={FULL_NAME_TOOLTIP_DELAY_SECONDS}
|
|
placement="topLeft"
|
|
>
|
|
<span
|
|
className="gn-query-toolbar-select-full-name"
|
|
aria-label={fullNameText}
|
|
>
|
|
{fullNameText}
|
|
</span>
|
|
</Tooltip>
|
|
);
|
|
};
|
|
|
|
const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
|
isV2Ui,
|
|
currentConnectionId,
|
|
currentDb,
|
|
queryCapableConnections,
|
|
dbList,
|
|
maxRows,
|
|
sqlEditorCommitMode,
|
|
sqlEditorAutoCommitDelayMs,
|
|
pendingTransactionToolbar,
|
|
runQueryShortcutBinding,
|
|
saveQueryShortcutBinding,
|
|
formatSqlShortcutBinding,
|
|
triggerSqlAiCompletionShortcutBinding,
|
|
toggleQueryResultsPanelShortcutBinding,
|
|
activeShortcutPlatform,
|
|
isResultPanelVisible,
|
|
loading,
|
|
saveMoreMenuItems,
|
|
formatSettingsMenu,
|
|
onConnectionChange,
|
|
onDatabaseChange,
|
|
onMaxRowsChange,
|
|
onCommitModeChange,
|
|
onAutoCommitDelayMsChange,
|
|
onCaptureEditorCursorPosition,
|
|
onRun,
|
|
onCancel,
|
|
onQuickSave,
|
|
onFindInEditor,
|
|
onFormat,
|
|
onTriggerSqlAiCompletion,
|
|
onToggleResultPanelVisibility,
|
|
onAIAction,
|
|
}) => {
|
|
const i18n = useOptionalI18n();
|
|
const t = i18n?.t ?? defaultTranslate;
|
|
const baseMoreMenuItems = saveMoreMenuItems ?? [];
|
|
const connectionSelectOptions: FullNameSelectOption[] =
|
|
queryCapableConnections.map((connection) => ({
|
|
label: connection.name,
|
|
value: connection.id,
|
|
title: "",
|
|
fullName: connection.name,
|
|
}));
|
|
const databaseSelectOptions: FullNameSelectOption[] = dbList.map((db) => ({
|
|
label: db,
|
|
value: db,
|
|
title: "",
|
|
fullName: db,
|
|
}));
|
|
const toggleResultPanelShortcutLabel =
|
|
toggleQueryResultsPanelShortcutBinding.enabled &&
|
|
toggleQueryResultsPanelShortcutBinding.combo
|
|
? getShortcutDisplayLabel(
|
|
toggleQueryResultsPanelShortcutBinding.combo,
|
|
activeShortcutPlatform,
|
|
)
|
|
: "";
|
|
const toggleResultPanelTitle =
|
|
toggleQueryResultsPanelShortcutBinding.enabled &&
|
|
toggleQueryResultsPanelShortcutBinding.combo
|
|
? t(
|
|
isResultPanelVisible
|
|
? "query_editor.action.hide_results_panel_with_shortcut"
|
|
: "query_editor.action.show_results_panel_with_shortcut",
|
|
{ shortcut: toggleResultPanelShortcutLabel },
|
|
)
|
|
: isResultPanelVisible
|
|
? t("query_editor.action.hide_results_panel")
|
|
: t("query_editor.action.show_results_panel");
|
|
const formatSqlTitle =
|
|
formatSqlShortcutBinding.enabled && formatSqlShortcutBinding.combo
|
|
? t("query_editor.action.format_sql_with_shortcut", {
|
|
shortcut: getShortcutDisplayLabel(
|
|
formatSqlShortcutBinding.combo,
|
|
activeShortcutPlatform,
|
|
),
|
|
})
|
|
: t("query_editor.action.format_sql");
|
|
const findInEditorShortcutCombo =
|
|
activeShortcutPlatform === "mac" ? "Meta+F" : "Ctrl+F";
|
|
const findInEditorTitle = t(
|
|
"query_editor.action.find_in_editor_with_shortcut",
|
|
{
|
|
shortcut: getShortcutDisplayLabel(
|
|
findInEditorShortcutCombo,
|
|
activeShortcutPlatform,
|
|
),
|
|
},
|
|
);
|
|
const triggerSqlAiCompletionLabel =
|
|
triggerSqlAiCompletionShortcutBinding.enabled &&
|
|
triggerSqlAiCompletionShortcutBinding.combo
|
|
? `${t("app.shortcuts.action.triggerSqlAiCompletion.label")} · ${getShortcutDisplayLabel(
|
|
triggerSqlAiCompletionShortcutBinding.combo,
|
|
activeShortcutPlatform,
|
|
)}`
|
|
: t("app.shortcuts.action.triggerSqlAiCompletion.label");
|
|
const aiMenuItems: MenuProps["items"] = [
|
|
{
|
|
key: "ai-inline-completion",
|
|
label: triggerSqlAiCompletionLabel,
|
|
icon: <RobotOutlined />,
|
|
onClick: onTriggerSqlAiCompletion,
|
|
},
|
|
{ type: "divider" as const },
|
|
{
|
|
key: "ai-generate",
|
|
label: t("query_editor.action.ai_text_to_sql_menu"),
|
|
icon: <RobotOutlined />,
|
|
onClick: () => onAIAction("generate"),
|
|
},
|
|
{
|
|
key: "ai-explain",
|
|
label: t("query_editor.action.ai_explain_sql_menu"),
|
|
icon: <RobotOutlined />,
|
|
onClick: () => onAIAction("explain"),
|
|
},
|
|
{
|
|
key: "ai-optimize",
|
|
label: t("query_editor.action.ai_optimize_sql_menu"),
|
|
icon: <RobotOutlined />,
|
|
onClick: () => onAIAction("optimize"),
|
|
},
|
|
{ type: "divider" as const },
|
|
{
|
|
key: "ai-schema",
|
|
label: t("query_editor.action.ai_schema_analysis"),
|
|
icon: <RobotOutlined />,
|
|
onClick: () => onAIAction("schema"),
|
|
},
|
|
];
|
|
const moreMenuItems: MenuProps["items"] = isV2Ui
|
|
? [
|
|
...baseMoreMenuItems,
|
|
...(baseMoreMenuItems.length > 0 ? [{ type: "divider" as const }] : []),
|
|
{
|
|
key: "toggle-result-panel",
|
|
label: toggleResultPanelTitle,
|
|
icon: isResultPanelVisible ? (
|
|
<EyeInvisibleOutlined />
|
|
) : (
|
|
<EyeOutlined />
|
|
),
|
|
onClick: onToggleResultPanelVisibility,
|
|
},
|
|
]
|
|
: baseMoreMenuItems;
|
|
const selects = (
|
|
<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={t("query_editor.placeholder.connection")}
|
|
value={currentConnectionId}
|
|
onChange={onConnectionChange}
|
|
options={connectionSelectOptions}
|
|
optionFilterProp="label"
|
|
optionRender={(option) => renderFullNameSelectTooltip(option.data.fullName)}
|
|
labelRender={(option) => renderFullNameSelectTooltip(option.label ?? option.value)}
|
|
showSearch
|
|
/>
|
|
<Select
|
|
className={
|
|
isV2Ui
|
|
? "gn-v2-query-toolbar-select gn-v2-query-toolbar-database-select"
|
|
: undefined
|
|
}
|
|
style={isV2Ui ? undefined : { width: 200 }}
|
|
placeholder={t("query_editor.placeholder.database")}
|
|
value={currentDb}
|
|
onChange={onDatabaseChange}
|
|
options={databaseSelectOptions}
|
|
optionFilterProp="label"
|
|
optionRender={(option) => renderFullNameSelectTooltip(option.data.fullName)}
|
|
labelRender={(option) => renderFullNameSelectTooltip(option.label ?? option.value)}
|
|
showSearch
|
|
/>
|
|
<Tooltip title={t("query_editor.max_rows.tooltip")}>
|
|
<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: t("query_editor.max_rows.option_500"), value: 500 },
|
|
{ label: t("query_editor.max_rows.option_1000"), value: 1000 },
|
|
{ label: t("query_editor.max_rows.option_5000"), value: 5000 },
|
|
{ label: t("query_editor.max_rows.option_20000"), value: 20000 },
|
|
{ label: t("query_editor.max_rows.option_unlimited"), value: 0 },
|
|
]}
|
|
/>
|
|
</Tooltip>
|
|
<QueryEditorTransactionSettings
|
|
isV2Ui={isV2Ui}
|
|
commitMode={sqlEditorCommitMode}
|
|
autoCommitDelayMs={sqlEditorAutoCommitDelayMs}
|
|
onCommitModeChange={onCommitModeChange}
|
|
onAutoCommitDelayMsChange={onAutoCommitDelayMsChange}
|
|
/>
|
|
{!isV2Ui && pendingTransactionToolbar}
|
|
</div>
|
|
);
|
|
|
|
const actions = (
|
|
<div
|
|
className={isV2Ui ? "gn-v2-query-toolbar-actions" : undefined}
|
|
style={{
|
|
display: "flex",
|
|
gap: "8px",
|
|
flexShrink: 0,
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<div
|
|
className={isV2Ui ? "gn-v2-query-toolbar-action-group" : undefined}
|
|
style={{ display: "flex", gap: "8px", alignItems: "center" }}
|
|
>
|
|
<Tooltip
|
|
title={
|
|
runQueryShortcutBinding.enabled && runQueryShortcutBinding.combo
|
|
? t("query_editor.action.run_with_shortcut", {
|
|
shortcut: getShortcutDisplayLabel(
|
|
runQueryShortcutBinding.combo,
|
|
activeShortcutPlatform,
|
|
),
|
|
})
|
|
: t("query_editor.action.run")
|
|
}
|
|
>
|
|
<Button
|
|
className={isV2Ui ? "gn-v2-query-toolbar-run-action" : undefined}
|
|
type="primary"
|
|
icon={<PlayCircleOutlined />}
|
|
onMouseDown={onCaptureEditorCursorPosition}
|
|
onClick={onRun}
|
|
loading={loading}
|
|
>
|
|
{t("query_editor.action.run")}
|
|
</Button>
|
|
</Tooltip>
|
|
{loading && (
|
|
<Button
|
|
type="primary"
|
|
danger
|
|
icon={<StopOutlined />}
|
|
onClick={onCancel}
|
|
>
|
|
{t("query_editor.action.stop")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
{isV2Ui && pendingTransactionToolbar}
|
|
<div
|
|
className={isV2Ui ? "gn-v2-query-toolbar-action-pair" : undefined}
|
|
style={{ display: "flex", gap: "8px", alignItems: "center" }}
|
|
>
|
|
<Tooltip
|
|
title={
|
|
saveQueryShortcutBinding.enabled && saveQueryShortcutBinding.combo
|
|
? t("query_editor.action.save_with_shortcut", {
|
|
shortcut: getShortcutDisplayLabel(
|
|
saveQueryShortcutBinding.combo,
|
|
activeShortcutPlatform,
|
|
),
|
|
})
|
|
: t("query_editor.action.save")
|
|
}
|
|
>
|
|
<Button icon={<SaveOutlined />} onClick={onQuickSave}>
|
|
{t("query_editor.action.save")}
|
|
</Button>
|
|
</Tooltip>
|
|
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
|
|
<Tooltip title={triggerSqlAiCompletionLabel}>
|
|
<Button
|
|
className={isV2Ui ? "gn-v2-query-toolbar-ai-action" : undefined}
|
|
icon={<RobotOutlined />}
|
|
style={{ color: "#818cf8" }}
|
|
onMouseDown={onCaptureEditorCursorPosition}
|
|
onClick={onTriggerSqlAiCompletion}
|
|
>
|
|
AI
|
|
</Button>
|
|
</Tooltip>
|
|
<Dropdown
|
|
menu={{ items: aiMenuItems }}
|
|
placement="bottomRight"
|
|
trigger={["click"]}
|
|
>
|
|
<Button
|
|
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
|
icon={<DownOutlined />}
|
|
aria-label="AI more actions"
|
|
/>
|
|
</Dropdown>
|
|
</div>
|
|
<Dropdown
|
|
menu={{ items: moreMenuItems }}
|
|
placement="bottomRight"
|
|
trigger={["click"]}
|
|
>
|
|
<Button>{t("query_editor.action.more")}</Button>
|
|
</Dropdown>
|
|
</div>
|
|
|
|
<div
|
|
className={isV2Ui ? "gn-v2-query-toolbar-action-pair" : undefined}
|
|
style={{ display: "flex", gap: "8px", alignItems: "center" }}
|
|
>
|
|
<Tooltip title={findInEditorTitle}>
|
|
<Button icon={<SearchOutlined />} onClick={onFindInEditor}>
|
|
{t("query_editor.action.find_in_editor")}
|
|
</Button>
|
|
</Tooltip>
|
|
<Tooltip title={formatSqlTitle}>
|
|
<Button icon={<FormatPainterOutlined />} onClick={onFormat}>
|
|
{t("query_editor.action.format")}
|
|
</Button>
|
|
</Tooltip>
|
|
<Dropdown
|
|
menu={{ items: formatSettingsMenu }}
|
|
placement="bottomRight"
|
|
trigger={["click"]}
|
|
>
|
|
<Button
|
|
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
|
icon={<SettingOutlined />}
|
|
/>
|
|
</Dropdown>
|
|
</div>
|
|
|
|
{!isV2Ui && (
|
|
<Tooltip title={toggleResultPanelTitle}>
|
|
<Button
|
|
icon={
|
|
isResultPanelVisible ? <EyeInvisibleOutlined /> : <EyeOutlined />
|
|
}
|
|
onClick={onToggleResultPanelVisibility}
|
|
>
|
|
{t("query_editor.action.results")}
|
|
</Button>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
if (!isV2Ui) {
|
|
return (
|
|
<div
|
|
className={undefined}
|
|
style={{
|
|
padding: "4px 8px 8px",
|
|
display: "flex",
|
|
gap: "8px",
|
|
flexShrink: 0,
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
{selects}
|
|
{actions}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="gn-v2-query-toolbar"
|
|
style={{
|
|
padding: "4px 8px 8px",
|
|
display: "flex",
|
|
gap: "8px",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<div
|
|
className="gn-v2-query-toolbar-main"
|
|
style={{
|
|
display: "flex",
|
|
gap: "8px",
|
|
flexShrink: 0,
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
{selects}
|
|
{actions}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default QueryEditorToolbar;
|