From 59c13c3366a4ba4df932ab19f97d7cea13f75216 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 9 Oct 2025 11:04:36 +0800 Subject: [PATCH] refactor(mcp): redesign MCP management panel UI - Redesign MCP panel to match main interface style - Add toggle switch for each MCP server to enable/disable - Use emerald theme color consistent with MCP button - Create card-based layout with one MCP per row - Add dedicated form modal for add/edit operations - Implement proper empty state with friendly prompts - Add comprehensive i18n support (zh/en) - Extend McpServer type to support enabled field - Backend already supports enabled field via serde_json::Value Components: - McpPanel: Main panel container with header and list - McpListItem: Card-based list item with toggle and actions - McpFormModal: Independent modal for add/edit forms - McpToggle: Emerald-themed toggle switch component All changes passed TypeScript type checking and production build. --- src/App.tsx | 5 +- src/components/mcp/McpFormModal.tsx | 250 +++++++++++++++ src/components/mcp/McpListItem.tsx | 84 +++++ src/components/mcp/McpPanel.tsx | 454 ++++++++++------------------ src/components/mcp/McpToggle.tsx | 41 +++ src/i18n/locales/en.json | 12 +- src/i18n/locales/zh.json | 12 +- src/lib/tauri-api.ts | 8 +- src/types.ts | 1 + 9 files changed, 566 insertions(+), 301 deletions(-) create mode 100644 src/components/mcp/McpFormModal.tsx create mode 100644 src/components/mcp/McpListItem.tsx create mode 100644 src/components/mcp/McpToggle.tsx diff --git a/src/App.tsx b/src/App.tsx index 8a5ef6d..d6c5b12 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -391,7 +391,10 @@ function App() { )} {isMcpOpen && ( - setIsMcpOpen(false)} onNotify={showNotification} /> + setIsMcpOpen(false)} + onNotify={showNotification} + /> )} ); diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx new file mode 100644 index 0000000..c509e2d --- /dev/null +++ b/src/components/mcp/McpFormModal.tsx @@ -0,0 +1,250 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { X, Save, Wrench } from "lucide-react"; +import { McpServer } from "../../types"; +import { buttonStyles, inputStyles } from "../../lib/styles"; + +interface McpFormModalProps { + editingId?: string; + initialData?: McpServer; + onSave: (id: string, server: McpServer) => void; + onClose: () => void; +} + +const parseEnvText = (text: string): Record => { + const lines = text + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.length > 0); + const env: Record = {}; + for (const l of lines) { + const idx = l.indexOf("="); + if (idx > 0) { + const k = l.slice(0, idx).trim(); + const v = l.slice(idx + 1).trim(); + if (k) env[k] = v; + } + } + return env; +}; + +const formatEnvText = (env?: Record): string => { + if (!env) return ""; + return Object.entries(env) + .map(([k, v]) => `${k}=${v}`) + .join("\n"); +}; + +/** + * MCP 表单模态框组件 + * 用于添加或编辑 MCP 服务器 + */ +const McpFormModal: React.FC = ({ + editingId, + initialData, + onSave, + onClose, +}) => { + const { t } = useTranslation(); + const [formId, setFormId] = useState(editingId || ""); + const [formType, setFormType] = useState<"stdio" | "sse">( + initialData?.type || "stdio", + ); + const [formCommand, setFormCommand] = useState(initialData?.command || ""); + const [formArgsText, setFormArgsText] = useState( + (initialData?.args || []).join(" "), + ); + const [formEnvText, setFormEnvText] = useState( + formatEnvText(initialData?.env), + ); + const [formCwd, setFormCwd] = useState(initialData?.cwd || ""); + const [saving, setSaving] = useState(false); + + // 编辑模式下禁止修改 ID + const isEditing = !!editingId; + + const handleValidateCommand = async () => { + if (!formCommand) return; + try { + const ok = await window.api.validateMcpCommand(formCommand.trim()); + const message = ok ? t("mcp.validation.ok") : t("mcp.validation.fail"); + // 这里简单使用 alert,实际项目中应该使用 notification 系统 + alert(message); + } catch (_error) { + alert(t("mcp.validation.fail")); + } + }; + + const handleSubmit = async () => { + if (!formId.trim()) { + alert(t("mcp.error.idRequired")); + return; + } + if (!formCommand.trim()) { + alert(t("mcp.error.commandRequired")); + return; + } + + setSaving(true); + try { + const server: McpServer = { + type: formType, + command: formCommand.trim(), + args: formArgsText + .split(/\s+/) + .map((s) => s.trim()) + .filter((s) => s.length > 0), + env: parseEnvText(formEnvText), + ...(formCwd ? { cwd: formCwd } : {}), + // 保留原有的 enabled 状态 + ...(initialData?.enabled !== undefined + ? { enabled: initialData.enabled } + : {}), + }; + + onSave(formId.trim(), server); + } finally { + setSaving(false); + } + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+

+ {isEditing ? t("mcp.editServer") : t("mcp.addServer")} +

+ +
+ + {/* Content */} +
+ {/* ID */} +
+ + setFormId(e.target.value)} + disabled={isEditing} + /> +
+ + {/* Type & CWD */} +
+
+ + +
+
+ + setFormCwd(e.target.value)} + /> +
+
+ + {/* Command */} +
+ +
+ setFormCommand(e.target.value)} + /> + +
+
+ + {/* Args */} +
+ + setFormArgsText(e.target.value)} + /> +
+ + {/* Env */} +
+ +