Feat/improve UI mcp settings (#4717)

* feat(MCPSettings): implement server selection and navigation with back button

* chore(ui)

* chore(UI): npx search padding

* feat(NpxSearch): add server selection and navigation; update styles

---------

Co-authored-by: eeee0717 <chentao020717Work@outlook.com>
This commit is contained in:
LiuVaayne 2025-04-12 15:31:52 +08:00 committed by GitHub
parent 10225512f4
commit 8eb6632620
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 215 additions and 110 deletions

View File

@ -9,7 +9,7 @@ import { Button, Card, Flex, Input, Space, Spin, Tag, Typography } from 'antd'
import { npxFinder } from 'npx-scope-finder'
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'
import styled from 'styled-components'
import { SettingDivider, SettingGroup, SettingTitle } from '..'
@ -27,7 +27,10 @@ const npmScopes = ['@cherry', '@modelcontextprotocol', '@gongrzhe', '@mcpmarket'
let _searchResults: SearchResult[] = []
const NpxSearch: FC = () => {
const NpxSearch: FC<{
setSelectedMcpServer: (server: MCPServer) => void
setRoute: (route: string | null) => void
}> = ({ setSelectedMcpServer, setRoute }) => {
const { theme } = useTheme()
const { t } = useTranslation()
const { Text, Link } = Typography
@ -116,7 +119,7 @@ const NpxSearch: FC = () => {
}, [])
return (
<SettingGroup theme={theme} css={SettingGroupCss}>
<SettingGroup theme={theme} css={SettingGroup}>
<div>
<SettingTitle>
{t('settings.mcp.npx_list.title')} <Text type="secondary">{t('settings.mcp.npx_list.desc')}</Text>
@ -181,10 +184,11 @@ const NpxSearch: FC = () => {
if (buildInServer) {
addMCPServer(buildInServer)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
setSelectedMcpServer(buildInServer)
setRoute(null)
return
}
addMCPServer({
const newServer = {
id: nanoid(),
name: record.name,
description: `${record.description}\n\n${t('settings.mcp.npx_list.usage')}: ${record.usage}\n${t('settings.mcp.npx_list.npm')}: ${record.npmLink}`,
@ -192,8 +196,12 @@ const NpxSearch: FC = () => {
args: ['-y', record.fullName],
isActive: false,
type: record.type
})
}
addMCPServer(newServer)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
setSelectedMcpServer(newServer)
setRoute(null)
}}
/>
</Flex>
@ -215,14 +223,6 @@ const NpxSearch: FC = () => {
)
}
const SettingGroupCss = css`
height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 0;
`
const ResultList = styled.div`
flex: 1;
display: flex;

View File

@ -1,17 +1,13 @@
import { CodeOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'
import { ArrowLeftOutlined, CodeOutlined, PlusOutlined } from '@ant-design/icons'
import { nanoid } from '@reduxjs/toolkit'
import DragableList from '@renderer/components/DragableList'
import IndicatorLight from '@renderer/components/IndicatorLight'
import { HStack, VStack } from '@renderer/components/Layout'
import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { EventEmitter } from '@renderer/services/EventService'
import { MCPServer } from '@renderer/types'
import { Dropdown, MenuProps } from 'antd'
import { isEmpty } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { FC, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -22,8 +18,8 @@ import NpxSearch from './NpxSearch'
const MCPSettings: FC = () => {
const { t } = useTranslation()
const { mcpServers, addMCPServer, updateMcpServers, deleteMCPServer } = useMCPServers()
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
const { mcpServers, addMCPServer } = useMCPServers()
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(null)
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
const { theme } = useTheme()
@ -49,50 +45,34 @@ const MCPSettings: FC = () => {
addMCPServer(newServer)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
setSelectedMcpServer(newServer)
setRoute(null)
}
const onDeleteMcpServer = useCallback(
async (server: MCPServer) => {
try {
await window.api.mcp.removeServer(server)
await deleteMCPServer(server.id)
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
} catch (error: any) {
window.message.error({
content: `${t('settings.mcp.deleteError')}: ${error.message}`,
key: 'mcp-list'
})
}
},
[deleteMCPServer, t]
)
const getMenuItems = useCallback(
(server: MCPServer) => {
const menus: MenuProps['items'] = [
{
label: t('common.delete'),
danger: true,
key: 'delete',
icon: <DeleteOutlined />,
onClick: () => onDeleteMcpServer(server)
}
]
return menus
},
[onDeleteMcpServer, t]
)
useEffect(() => {
const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id)
setSelectedMcpServer(_selectedMcpServer || mcpServers[0])
}, [mcpServers, route, selectedMcpServer])
useEffect(() => {
// Check if the selected server still exists in the updated mcpServers list
if (selectedMcpServer) {
const serverExists = mcpServers.some((server) => server.id === selectedMcpServer.id)
if (!serverExists) {
setSelectedMcpServer(null)
}
} else {
setSelectedMcpServer(null)
}
}, [mcpServers, selectedMcpServer])
const MainContent = useMemo(() => {
if (route === 'npx-search' || isEmpty(mcpServers)) {
return (
<SettingContainer theme={theme}>
<NpxSearch />
<NpxSearch
setRoute={(route) => setRoute(route as 'npx-search' | 'mcp-install' | null)}
setSelectedMcpServer={setSelectedMcpServer}
/>
</SettingContainer>
)
}
@ -108,53 +88,70 @@ const MCPSettings: FC = () => {
return <McpSettings server={selectedMcpServer} />
}
return <NpxSearch />
return (
<NpxSearch
setRoute={(route) => setRoute(route as 'npx-search' | 'mcp-install' | null)}
setSelectedMcpServer={setSelectedMcpServer}
/>
)
}, [mcpServers, route, selectedMcpServer, theme])
const goBackToGrid = () => {
setSelectedMcpServer(null)
}
return (
<Container>
<McpListContainer>
<McpList>
<ListItem
key="add"
title={t('settings.mcp.addServer')}
active={false}
onClick={onAddMcpServer}
icon={<PlusOutlined />}
titleStyle={{ fontWeight: 500 }}
style={{ width: '100%', marginTop: -2 }}
/>
<DragableList list={mcpServers} onUpdate={updateMcpServers}>
{(server: MCPServer) => (
<Dropdown menu={{ items: getMenuItems(server) }} trigger={['contextMenu']} key={server.id}>
<div>
<ListItem
key={server.id}
title={server.name}
active={selectedMcpServer?.id === server.id}
onClick={() => {
setSelectedMcpServer(server)
setRoute(null)
}}
titleStyle={{ fontWeight: 500 }}
icon={<CodeOutlined />}
rightContent={
<IndicatorLight
size={6}
color={server.isActive ? 'green' : 'var(--color-text-3)'}
animation={server.isActive}
shadow={false}
style={{ marginRight: 4 }}
/>
}
/>
</div>
</Dropdown>
)}
</DragableList>
</McpList>
</McpListContainer>
{MainContent}
{selectedMcpServer ? (
<DetailViewContainer>
<BackButtonContainer>
<BackButton onClick={goBackToGrid}>
<ArrowLeftOutlined /> {t('common.back')}
</BackButton>
</BackButtonContainer>
<DetailContent>{MainContent}</DetailContent>
</DetailViewContainer>
) : (
<GridContainer>
<GridHeader>
<h2>{t('settings.mcp.newServer')}</h2>
</GridHeader>
<ServersGrid>
<AddServerCard onClick={onAddMcpServer}>
<PlusOutlined style={{ fontSize: 24 }} />
<AddServerText>{t('settings.mcp.addServer')}</AddServerText>
</AddServerCard>
{mcpServers.map((server) => (
<ServerCard
key={server.id}
onClick={() => {
setSelectedMcpServer(server)
setRoute(null)
}}>
<ServerHeader>
<ServerIcon>
<CodeOutlined />
</ServerIcon>
<ServerName>{server.name}</ServerName>
<StatusIndicator>
<IndicatorLight
size={6}
color={server.isActive ? 'green' : 'var(--color-text-3)'}
animation={server.isActive}
shadow={false}
/>
</StatusIndicator>
</ServerHeader>
<ServerDescription>
{server.description &&
server.description.substring(0, 60) + (server.description.length > 60 ? '...' : '')}
</ServerDescription>
</ServerCard>
))}
</ServersGrid>
</GridContainer>
)}
</Container>
)
}
@ -163,22 +160,129 @@ const Container = styled(HStack)`
flex: 1;
`
const McpListContainer = styled(VStack)`
width: var(--settings-width);
border-right: 0.5px solid var(--color-border);
const GridContainer = styled(VStack)`
width: 100%;
height: calc(100vh - var(--navbar-height));
padding: 20px;
`
const McpList = styled(Scrollbar)`
display: flex;
flex: 1;
flex-direction: column;
gap: 5px;
const GridHeader = styled.div`
width: 100%;
padding: 12px;
.iconfont {
color: var(--color-text-2);
line-height: 16px;
padding-bottom: 16px;
h2 {
font-size: 20px;
margin: 0;
}
`
const ServersGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
width: 100%;
overflow-y: auto;
padding: 2px;
`
const ServerCard = styled.div`
display: flex;
flex-direction: column;
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.2s ease;
height: 140px;
background-color: var(--color-bg-1);
&:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
`
const ServerHeader = styled.div`
display: flex;
align-items: center;
margin-bottom: 12px;
`
const ServerIcon = styled.div`
font-size: 18px;
color: var(--color-primary);
margin-right: 8px;
`
const ServerName = styled.div`
font-weight: 500;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`
const StatusIndicator = styled.div`
margin-left: 8px;
`
const ServerDescription = styled.div`
font-size: 12px;
color: var(--color-text-2);
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
`
const AddServerCard = styled(ServerCard)`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-style: dashed;
background-color: transparent;
color: var(--color-text-2);
`
const AddServerText = styled.div`
margin-top: 12px;
font-weight: 500;
`
const DetailViewContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
position: relative;
`
const BackButtonContainer = styled.div`
padding: 16px 0 0 20px;
width: 100%;
`
const DetailContent = styled.div`
flex: 1;
width: 100%;
`
const BackButton = styled.div`
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--color-text-1);
cursor: pointer;
padding: 6px 12px;
border-radius: 4px;
margin-bottom: 10px;
background-color: var(--color-bg-1);
&:hover {
color: var(--color-primary);
background-color: var(--color-bg-2);
}
`

View File

@ -10,6 +10,7 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
height: calc(100vh - var(--navbar-height));
padding: 20px;
padding-top: 15px;
padding-bottom: 75px;
overflow-y: scroll;
font-family: Ubuntu;
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};