feat: update localization strings and add EditMcpJsonPopup component

- Replaced "JSON Schema" and "Normal mode" with "Edit JSON" in localization files for English, Japanese, Russian, Simplified Chinese, and Traditional Chinese.
- Introduced a new EditMcpJsonPopup component for editing MCP server configurations in JSON format, enhancing user experience and modularity in MCPSettings.
This commit is contained in:
kangfenmao 2025-03-17 12:00:07 +08:00
parent ed59e0f47e
commit 94ba450323
9 changed files with 192 additions and 164 deletions

View File

@ -846,8 +846,7 @@
"no_packages": "No packages found", "no_packages": "No packages found",
"search_error": "Search error" "search_error": "Search error"
}, },
"jsonMode": "JSON Schema", "editJson": "Edit JSON",
"normalMode": "Normal mode",
"jsonModeHint": "Edit the JSON representation of the MCP server configuration. Please ensure the format is correct before saving.", "jsonModeHint": "Edit the JSON representation of the MCP server configuration. Please ensure the format is correct before saving.",
"jsonFormatError": "JSON formatting error", "jsonFormatError": "JSON formatting error",
"jsonSaveSuccess": "JSON configuration has been saved.", "jsonSaveSuccess": "JSON configuration has been saved.",

View File

@ -846,8 +846,7 @@
"no_packages": "パッケージが見つかりません", "no_packages": "パッケージが見つかりません",
"search_error": "パッケージの検索に失敗しました" "search_error": "パッケージの検索に失敗しました"
}, },
"jsonMode": "JSONスキーマ", "editJson": "JSONを編集",
"normalMode": "通常モード",
"jsonModeHint": "MCPサーバー設定のJSON表現を編集します。保存する前に、フォーマットが正しいことを確認してください。", "jsonModeHint": "MCPサーバー設定のJSON表現を編集します。保存する前に、フォーマットが正しいことを確認してください。",
"jsonFormatError": "JSONフォーマットエラー", "jsonFormatError": "JSONフォーマットエラー",
"jsonSaveSuccess": "JSON設定が保存されました。", "jsonSaveSuccess": "JSON設定が保存されました。",

View File

@ -846,8 +846,7 @@
"no_packages": "Ничего не найдено", "no_packages": "Ничего не найдено",
"search_error": "Ошибка поиска" "search_error": "Ошибка поиска"
}, },
"jsonMode": "JSON-схема", "editJson": "Редактировать JSON",
"normalMode": "Обычный режим",
"jsonModeHint": "Редактируйте JSON-форматирование конфигурации сервера MCP. Перед сохранением убедитесь, что формат правильный.", "jsonModeHint": "Редактируйте JSON-форматирование конфигурации сервера MCP. Перед сохранением убедитесь, что формат правильный.",
"jsonFormatError": "Ошибка форматирования JSON", "jsonFormatError": "Ошибка форматирования JSON",
"jsonSaveSuccess": "JSON конфигурация сохранена", "jsonSaveSuccess": "JSON конфигурация сохранена",

View File

@ -846,8 +846,7 @@
"no_packages": "未找到包", "no_packages": "未找到包",
"search_error": "搜索失败" "search_error": "搜索失败"
}, },
"jsonMode": "JSON模式", "editJson": "编辑JSON",
"normalMode": "常规模式",
"jsonModeHint": "编辑MCP服务器配置的JSON表示。保存前请确保格式正确。", "jsonModeHint": "编辑MCP服务器配置的JSON表示。保存前请确保格式正确。",
"jsonFormatError": "JSON格式化错误", "jsonFormatError": "JSON格式化错误",
"jsonSaveSuccess": "JSON配置已保存", "jsonSaveSuccess": "JSON配置已保存",

View File

@ -846,8 +846,7 @@
"no_packages": "未找到包", "no_packages": "未找到包",
"search_error": "搜索失敗" "search_error": "搜索失敗"
}, },
"jsonMode": "JSON模式", "editJson": "編輯JSON",
"normalMode": "常規模式",
"jsonModeHint": "編輯MCP伺服器配置的JSON表示。保存前請確保格式正確。", "jsonModeHint": "編輯MCP伺服器配置的JSON表示。保存前請確保格式正確。",
"jsonFormatError": "JSON格式錯誤", "jsonFormatError": "JSON格式錯誤",
"jsonSaveSuccess": "JSON配置已儲存", "jsonSaveSuccess": "JSON配置已儲存",

View File

@ -0,0 +1,152 @@
import { TopView } from '@renderer/components/TopView'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setMCPServers } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types'
import { Modal, Typography } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const [jsonConfig, setJsonConfig] = useState('')
const [jsonSaving, setJsonSaving] = useState(false)
const [jsonError, setJsonError] = useState('')
const mcpServers = useAppSelector((state) => state.mcp.servers)
const dispatch = useAppDispatch()
const { t } = useTranslation()
const ipcRenderer = window.electron.ipcRenderer
useEffect(() => {
try {
const mcpServersObj: Record<string, any> = {}
mcpServers.forEach((server) => {
const { name, ...serverData } = server
mcpServersObj[name] = serverData
})
const standardFormat = {
mcpServers: mcpServersObj
}
const formattedJson = JSON.stringify(standardFormat, null, 2)
setJsonConfig(formattedJson)
setJsonError('')
} catch (error) {
console.error('Failed to format JSON:', error)
setJsonError(t('settings.mcp.jsonFormatError'))
}
}, [mcpServers, t])
const onOk = async () => {
setJsonSaving(true)
try {
if (!jsonConfig.trim()) {
dispatch(setMCPServers([]))
window.message.success(t('settings.mcp.jsonSaveSuccess'))
setJsonError('')
setJsonSaving(false)
return
}
const parsedConfig = JSON.parse(jsonConfig)
if (!parsedConfig.mcpServers || typeof parsedConfig.mcpServers !== 'object') {
throw new Error(t('settings.mcp.invalidMcpFormat'))
}
const serversArray: MCPServer[] = []
for (const [name, serverConfig] of Object.entries(parsedConfig.mcpServers)) {
const server: MCPServer = {
name,
isActive: false,
...(serverConfig as any)
}
serversArray.push(server)
}
dispatch(setMCPServers(serversArray))
ipcRenderer.send('mcp:servers-from-renderer', mcpServers)
window.message.success(t('settings.mcp.jsonSaveSuccess'))
setJsonError('')
setOpen(false)
} catch (error: any) {
console.error('Failed to save JSON config:', error)
setJsonError(error.message || t('settings.mcp.jsonSaveError'))
window.message.error(t('settings.mcp.jsonSaveError'))
} finally {
setJsonSaving(false)
}
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
EditMcpJsonPopup.hide = onCancel
return (
<Modal
title={t('settings.mcp.editJson')}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
width={800}
height="80vh"
loading={jsonSaving}
transitionName="ant-move-down"
centered>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Typography.Text type="secondary">
{jsonError ? <span style={{ color: 'red' }}>{jsonError}</span> : ''}
</Typography.Text>
</div>
<TextArea
value={jsonConfig}
onChange={(e) => setJsonConfig(e.target.value)}
style={{
width: '100%',
fontFamily: 'monospace',
minHeight: '60vh',
marginBottom: '16px'
}}
onFocus={() => setJsonError('')}
/>
<Typography.Text type="secondary">{t('settings.mcp.jsonModeHint')}</Typography.Text>
</Modal>
)
}
const TopViewKey = 'EditMcpJsonPopup'
export default class EditMcpJsonPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@ -6,8 +6,8 @@ import styled from 'styled-components'
import { SettingRow, SettingSubtitle } from '..' import { SettingRow, SettingSubtitle } from '..'
const InstallNpxUv: FC = () => { const InstallNpxUv: FC = () => {
const [isUvInstalled, setIsUvInstalled] = useState(false) const [isUvInstalled, setIsUvInstalled] = useState(true)
const [isBunInstalled, setIsBunInstalled] = useState(false) const [isBunInstalled, setIsBunInstalled] = useState(true)
const [isInstallingUv, setIsInstallingUv] = useState(false) const [isInstallingUv, setIsInstallingUv] = useState(false)
const [isInstallingBun, setIsInstallingBun] = useState(false) const [isInstallingBun, setIsInstallingBun] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()

View File

@ -67,10 +67,10 @@ const NpxSearch: FC = () => {
} }
return ( return (
<SettingGroup theme={theme}> <SettingGroup theme={theme} style={{ marginTop: 15 }}>
<SettingTitle>{t('settings.mcp.npx_list.title')}</SettingTitle> <SettingTitle>{t('settings.mcp.npx_list.title')}</SettingTitle>
<SettingDivider /> <SettingDivider />
<Paragraph type="secondary" style={{ margin: '0 0 20px 0' }}> <Paragraph type="secondary" style={{ margin: '0 0 10px 0' }}>
{t('settings.mcp.npx_list.desc')} {t('settings.mcp.npx_list.desc')}
</Paragraph> </Paragraph>
@ -82,7 +82,7 @@ const NpxSearch: FC = () => {
onChange={(e) => setNpmScope(e.target.value)} onChange={(e) => setNpmScope(e.target.value)}
onPressEnter={handleNpmSearch} onPressEnter={handleNpmSearch}
/> />
<Button type="primary" icon={<SearchOutlined />} onClick={handleNpmSearch} disabled={searchLoading}> <Button icon={<SearchOutlined />} onClick={handleNpmSearch} disabled={searchLoading}>
{t('settings.mcp.npx_list.search')} {t('settings.mcp.npx_list.search')}
</Button> </Button>
</Space.Compact> </Space.Compact>

View File

@ -1,14 +1,15 @@
import { DeleteOutlined, EditOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons' import { DeleteOutlined, EditOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { setMCPServers } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
import { Button, Card, Input, Space, Switch, Table, Tabs, Tag, Tooltip, Typography } from 'antd' import { Button, Space, Switch, Table, Tag, Tooltip, Typography } from 'antd'
import { FC, useState } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..' import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
import AddMcpServerPopup from './AddMcpServerPopup' import AddMcpServerPopup from './AddMcpServerPopup'
import EditMcpJsonPopup from './EditMcpJsonPopup'
import InstallNpxUv from './InstallNpxUv' import InstallNpxUv from './InstallNpxUv'
import NpxSearch from './NpxSearch' import NpxSearch from './NpxSearch'
@ -16,13 +17,6 @@ const MCPSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const { Paragraph, Text } = Typography const { Paragraph, Text } = Typography
const { TextArea } = Input
const [activeTab, setActiveTab] = useState('normal')
const [jsonConfig, setJsonConfig] = useState('')
const [jsonSaving, setJsonSaving] = useState(false)
const [jsonError, setJsonError] = useState('')
const dispatch = useAppDispatch()
const ipcRenderer = window.electron.ipcRenderer
const mcpServers = useAppSelector((state) => state.mcp.servers) const mcpServers = useAppSelector((state) => state.mcp.servers)
const handleDelete = (serverName: string) => { const handleDelete = (serverName: string) => {
@ -52,72 +46,6 @@ const MCPSettings: FC = () => {
} }
} }
const handleTabChange = (key: string) => {
setActiveTab(key)
if (key === 'json') {
try {
const mcpServersObj: Record<string, any> = {}
mcpServers.forEach((server) => {
const { name, ...serverData } = server
mcpServersObj[name] = serverData
})
const standardFormat = {
mcpServers: mcpServersObj
}
const formattedJson = JSON.stringify(standardFormat, null, 2)
setJsonConfig(formattedJson)
setJsonError('')
} catch (error) {
console.error('Failed to format JSON:', error)
setJsonError(t('settings.mcp.jsonFormatError'))
}
}
}
const handleSaveJson = async () => {
setJsonSaving(true)
try {
if (!jsonConfig.trim()) {
dispatch(setMCPServers([]))
window.message.success(t('settings.mcp.jsonSaveSuccess'))
setJsonError('')
setJsonSaving(false)
return
}
const parsedConfig = JSON.parse(jsonConfig)
if (!parsedConfig.mcpServers || typeof parsedConfig.mcpServers !== 'object') {
throw new Error(t('settings.mcp.invalidMcpFormat'))
}
const serversArray: MCPServer[] = []
for (const [name, serverConfig] of Object.entries(parsedConfig.mcpServers)) {
const server: MCPServer = {
name,
isActive: false,
...(serverConfig as any)
}
serversArray.push(server)
}
dispatch(setMCPServers(serversArray))
ipcRenderer.send('mcp:servers-from-renderer', mcpServers)
window.message.success(t('settings.mcp.jsonSaveSuccess'))
setJsonError('')
} catch (error: any) {
console.error('Failed to save JSON config:', error)
setJsonError(error.message || t('settings.mcp.jsonSaveError'))
window.message.error(t('settings.mcp.jsonSaveError'))
} finally {
setJsonSaving(false)
}
}
const columns = [ const columns = [
{ {
title: t('settings.mcp.name'), title: t('settings.mcp.name'),
@ -209,79 +137,32 @@ const MCPSettings: FC = () => {
</Tooltip> </Tooltip>
</SettingTitle> </SettingTitle>
<SettingDivider /> <SettingDivider />
<Paragraph type="secondary" style={{ margin: 0 }}> <HStack gap={15} alignItems="center">
{t('settings.mcp.config_description')} <Button type="primary" icon={<PlusOutlined />} onClick={() => AddMcpServerPopup.show()}>
</Paragraph> {t('settings.mcp.addServer')}
</Button>
<Tabs <Button icon={<EditOutlined />} onClick={() => EditMcpJsonPopup.show()}>
activeKey={activeTab} {t('settings.mcp.editJson')}
onChange={handleTabChange} </Button>
items={[ </HStack>
{
label: t('settings.mcp.normalMode'),
key: 'normal',
children: (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={() => AddMcpServerPopup.show()}>
{t('settings.mcp.addServer')}
</Button>
<Text type="secondary">
{mcpServers.length}{' '}
{mcpServers.length === 1 ? t('settings.mcp.serverSingular') : t('settings.mcp.serverPlural')}
</Text>
</div>
<Card
bordered={false}
style={{ background: theme === 'dark' ? '#1f1f1f' : '#fff' }}
styles={{ body: { padding: 0 } }}>
<Table
dataSource={mcpServers}
columns={columns}
rowKey="name"
pagination={false}
size="small"
locale={{ emptyText: t('settings.mcp.noServers') }}
rowClassName={(record) => (!record.isActive ? 'inactive-row' : '')}
onRow={(record) => ({
style: !record.isActive ? inactiveRowStyle : {}
})}
/>
</Card>
</>
)
},
{
label: t('settings.mcp.jsonMode'),
key: 'json',
children: (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Button type="primary" onClick={handleSaveJson} loading={jsonSaving}>
{t('common.save')}
</Button>
<Text type="secondary">{jsonError ? <span style={{ color: 'red' }}>{jsonError}</span> : ''}</Text>
</div>
<Card bordered={false} style={{ background: theme === 'dark' ? '#1f1f1f' : '#fff' }}>
<TextArea
value={jsonConfig}
onChange={(e) => setJsonConfig(e.target.value)}
style={{
width: '100%',
fontFamily: 'monospace',
minHeight: '400px',
marginBottom: '16px'
}}
onFocus={() => setJsonError('')}
/>
<Text type="secondary">{t('settings.mcp.jsonModeHint')}</Text>
</Card>
</>
)
}
]}></Tabs>
</SettingGroup> </SettingGroup>
<Table
dataSource={mcpServers}
columns={columns}
rowKey="name"
pagination={false}
size="small"
locale={{ emptyText: t('settings.mcp.noServers') }}
rowClassName={(record) => (!record.isActive ? 'inactive-row' : '')}
onRow={(record) => ({
style: !record.isActive ? inactiveRowStyle : {}
})}
style={{
borderRadius: '8px',
overflow: 'hidden',
border: '0.5px solid var(--color-border)'
}}
/>
<NpxSearch /> <NpxSearch />
</SettingContainer> </SettingContainer>
) )