From 9304653c344e572ffd5b59f5e5a8aa3ecd2c1444 Mon Sep 17 00:00:00 2001 From: samanhappy Date: Fri, 25 Jul 2025 14:30:52 +0800 Subject: [PATCH] feat: enhance GroupCard with copy options for ID, URL, and JSON; update translations (#246) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- frontend/src/components/GroupCard.tsx | 97 ++++++++++++++++--- frontend/src/components/icons/LucideIcons.tsx | 15 ++- frontend/src/hooks/useSettingsData.ts | 3 + frontend/src/locales/en.json | 6 ++ frontend/src/locales/zh.json | 6 ++ frontend/src/pages/SettingsPage.tsx | 30 +++++- src/controllers/serverController.ts | 9 +- src/types/index.ts | 1 + 8 files changed, 149 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/GroupCard.tsx b/frontend/src/components/GroupCard.tsx index 03564f1..059c816 100644 --- a/frontend/src/components/GroupCard.tsx +++ b/frontend/src/components/GroupCard.tsx @@ -1,9 +1,10 @@ -import { useState } from 'react' +import { useState, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Group, Server } from '@/types' -import { Edit, Trash, Copy, Check } from '@/components/icons/LucideIcons' +import { Edit, Trash, Copy, Check, Link, FileCode, DropdownIcon } from '@/components/icons/LucideIcons' import DeleteDialog from '@/components/ui/DeleteDialog' import { useToast } from '@/contexts/ToastContext' +import { useSettingsData } from '@/hooks/useSettingsData' interface GroupCardProps { group: Group @@ -20,8 +21,25 @@ const GroupCard = ({ }: GroupCardProps) => { const { t } = useTranslation() const { showToast } = useToast() + const { installConfig } = useSettingsData() const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [copied, setCopied] = useState(false) + const [showCopyDropdown, setShowCopyDropdown] = useState(false) + const dropdownRef = useRef(null) + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setShowCopyDropdown(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) const handleEdit = () => { onEdit(group) @@ -36,16 +54,18 @@ const GroupCard = ({ setShowDeleteDialog(false) } - const copyToClipboard = () => { + const copyToClipboard = (text: string) => { if (navigator.clipboard && window.isSecureContext) { - navigator.clipboard.writeText(group.id).then(() => { + navigator.clipboard.writeText(text).then(() => { setCopied(true) + setShowCopyDropdown(false) + showToast(t('common.copySuccess'), 'success') setTimeout(() => setCopied(false), 2000) }) } else { // Fallback for HTTP or unsupported clipboard API const textArea = document.createElement('textarea') - textArea.value = group.id + textArea.value = text // Avoid scrolling to bottom textArea.style.position = 'fixed' textArea.style.left = '-9999px' @@ -55,6 +75,8 @@ const GroupCard = ({ try { document.execCommand('copy') setCopied(true) + setShowCopyDropdown(false) + showToast(t('common.copySuccess'), 'success') setTimeout(() => setCopied(false), 2000) } catch (err) { showToast(t('common.copyFailed') || 'Copy failed', 'error') @@ -64,6 +86,28 @@ const GroupCard = ({ } } + const handleCopyId = () => { + copyToClipboard(group.id) + } + + const handleCopyUrl = () => { + copyToClipboard(`${installConfig.baseUrl}/mcp/${group.id}`) + } + + const handleCopyJson = () => { + const jsonConfig = { + mcpServers: { + mcphub: { + url: `${installConfig.baseUrl}/mcp/${group.id}`, + headers: { + Authorization: "Bearer " + } + } + } + } + copyToClipboard(JSON.stringify(jsonConfig, null, 2)) + } + // Get servers that belong to this group const groupServers = servers.filter(server => group.servers.includes(server.name)) @@ -75,13 +119,42 @@ const GroupCard = ({

{group.name}

{group.id} - +
+ + + {showCopyDropdown && ( +
+ + + +
+ )} +
{group.description && ( diff --git a/frontend/src/components/icons/LucideIcons.tsx b/frontend/src/components/icons/LucideIcons.tsx index eceab0d..28ad87f 100644 --- a/frontend/src/components/icons/LucideIcons.tsx +++ b/frontend/src/components/icons/LucideIcons.tsx @@ -13,7 +13,10 @@ import { Loader, CheckCircle, XCircle, - AlertCircle + AlertCircle, + Link, + FileCode, + ChevronDown as DropdownIcon } from 'lucide-react' export { @@ -31,7 +34,10 @@ export { Loader, CheckCircle, XCircle, - AlertCircle + AlertCircle, + Link, + FileCode, + DropdownIcon } const LucideIcons = { @@ -49,7 +55,10 @@ const LucideIcons = { Loader, CheckCircle, XCircle, - AlertCircle + AlertCircle, + Link, + FileCode, + DropdownIcon } export default LucideIcons \ No newline at end of file diff --git a/frontend/src/hooks/useSettingsData.ts b/frontend/src/hooks/useSettingsData.ts index d0e7fe6..411de9e 100644 --- a/frontend/src/hooks/useSettingsData.ts +++ b/frontend/src/hooks/useSettingsData.ts @@ -16,6 +16,7 @@ interface RoutingConfig { interface InstallConfig { pythonIndexUrl: string; npmRegistry: string; + baseUrl: string; } interface SmartRoutingConfig { @@ -57,6 +58,7 @@ export const useSettingsData = () => { const [installConfig, setInstallConfig] = useState({ pythonIndexUrl: '', npmRegistry: '', + baseUrl: 'http://localhost:3000', }); const [smartRoutingConfig, setSmartRoutingConfig] = useState({ @@ -108,6 +110,7 @@ export const useSettingsData = () => { setInstallConfig({ pythonIndexUrl: data.data.systemConfig.install.pythonIndexUrl || '', npmRegistry: data.data.systemConfig.install.npmRegistry || '', + baseUrl: data.data.systemConfig.install.baseUrl || 'http://localhost:3000', }); } if (data.success && data.data?.systemConfig?.smartRouting) { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 45fb346..72e450b 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -180,6 +180,9 @@ "delete": "Delete", "remove": "Remove", "copy": "Copy", + "copyId": "Copy ID", + "copyUrl": "Copy URL", + "copyJson": "Copy JSON", "copySuccess": "Copied to clipboard", "copyFailed": "Copy failed", "close": "Close", @@ -366,6 +369,9 @@ "npmRegistry": "NPM Registry URL", "npmRegistryDescription": "Set npm_config_registry environment variable for NPM package installation", "npmRegistryPlaceholder": "e.g. https://registry.npmjs.org/", + "baseUrl": "Base URL", + "baseUrlDescription": "Base URL for MCP requests", + "baseUrlPlaceholder": "e.g. http://localhost:3000", "installConfig": "Installation", "systemConfigUpdated": "System configuration updated successfully", "enableSmartRouting": "Enable Smart Routing", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 5e94d80..a246d1b 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -181,6 +181,9 @@ "delete": "删除", "remove": "移除", "copy": "复制", + "copyId": "复制ID", + "copyUrl": "复制URL", + "copyJson": "复制JSON", "copySuccess": "已复制到剪贴板", "copyFailed": "复制失败", "close": "关闭", @@ -367,6 +370,9 @@ "npmRegistry": "NPM 仓库地址", "npmRegistryDescription": "设置 npm_config_registry 环境变量,用于 NPM 包安装", "npmRegistryPlaceholder": "例如: https://registry.npmmirror.com/", + "baseUrl": "基础地址", + "baseUrlDescription": "用于 MCP 请求的基础地址", + "baseUrlPlaceholder": "例如: http://localhost:3000", "installConfig": "安装配置", "systemConfigUpdated": "系统配置更新成功", "enableSmartRouting": "启用智能路由", diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 7a306f1..c2c37d0 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -23,9 +23,11 @@ const SettingsPage: React.FC = () => { const [installConfig, setInstallConfig] = useState<{ pythonIndexUrl: string; npmRegistry: string; + baseUrl: string; }>({ pythonIndexUrl: '', npmRegistry: '', + baseUrl: 'http://localhost:3000', }); const [tempSmartRoutingConfig, setTempSmartRoutingConfig] = useState<{ @@ -125,14 +127,14 @@ const SettingsPage: React.FC = () => { await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey); }; - const handleInstallConfigChange = (key: 'pythonIndexUrl' | 'npmRegistry', value: string) => { + const handleInstallConfigChange = (key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl', value: string) => { setInstallConfig({ ...installConfig, [key]: value }); }; - const saveInstallConfig = async (key: 'pythonIndexUrl' | 'npmRegistry') => { + const saveInstallConfig = async (key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl') => { await updateInstallConfig(key, installConfig[key]); }; @@ -467,6 +469,30 @@ const SettingsPage: React.FC = () => { {sectionsVisible.installConfig && (
+
+
+

{t('settings.baseUrl')}

+

{t('settings.baseUrlDescription')}

+
+
+ handleInstallConfigChange('baseUrl', e.target.value)} + placeholder={t('settings.baseUrlPlaceholder')} + className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input" + disabled={loading} + /> + +
+
+

{t('settings.pythonIndexUrl')}

diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index a78cd62..bbcd335 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -515,7 +515,9 @@ export const updateSystemConfig = (req: Request, res: Response): void => { typeof routing.bearerAuthKey !== 'string' && typeof routing.skipAuth !== 'boolean')) && (!install || - (typeof install.pythonIndexUrl !== 'string' && typeof install.npmRegistry !== 'string')) && + (typeof install.pythonIndexUrl !== 'string' && + typeof install.npmRegistry !== 'string' && + typeof install.baseUrl !== 'string')) && (!smartRouting || (typeof smartRouting.enabled !== 'boolean' && typeof smartRouting.dbUrl !== 'string' && @@ -543,6 +545,7 @@ export const updateSystemConfig = (req: Request, res: Response): void => { install: { pythonIndexUrl: '', npmRegistry: '', + baseUrl: 'http://localhost:3000', }, smartRouting: { enabled: false, @@ -568,6 +571,7 @@ export const updateSystemConfig = (req: Request, res: Response): void => { settings.systemConfig.install = { pythonIndexUrl: '', npmRegistry: '', + baseUrl: 'http://localhost:3000', }; } @@ -610,6 +614,9 @@ export const updateSystemConfig = (req: Request, res: Response): void => { if (typeof install.npmRegistry === 'string') { settings.systemConfig.install.npmRegistry = install.npmRegistry; } + if (typeof install.baseUrl === 'string') { + settings.systemConfig.install.baseUrl = install.baseUrl; + } } // Track smartRouting state and configuration changes diff --git a/src/types/index.ts b/src/types/index.ts index 9552b8d..7377fa3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -86,6 +86,7 @@ export interface SystemConfig { install?: { pythonIndexUrl?: string; // Python package repository URL (UV_DEFAULT_INDEX) npmRegistry?: string; // NPM registry URL (npm_config_registry) + baseUrl?: string; // Base URL for group card copy operations }; smartRouting?: SmartRoutingConfig; }