feat(McpServersList): add a search bar (#9520)

* feat(McpServersList): add a search bar

* refactor: show different empty tips
This commit is contained in:
one 2025-08-25 20:35:48 +08:00 committed by GitHub
parent ffa2eb57b1
commit 548916e6e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 137 additions and 32 deletions

View File

@ -1,21 +1,30 @@
import i18n from '@renderer/i18n'
import { Input, InputRef, Tooltip } from 'antd'
import { Search } from 'lucide-react'
import { motion } from 'motion/react'
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
interface CollapsibleSearchBarProps {
onSearch: (text: string) => void
placeholder?: string
tooltip?: string
icon?: React.ReactNode
maxWidth?: string | number
style?: React.CSSProperties
}
/**
* A collapsible search bar for list headers
* Renders as an icon initially, expands to full search input when clicked
*/
const CollapsibleSearchBar: React.FC<CollapsibleSearchBarProps> = ({ onSearch, icon, maxWidth }) => {
const { t } = useTranslation()
const CollapsibleSearchBar = ({
onSearch,
placeholder = i18n.t('common.search'),
tooltip = i18n.t('common.search'),
icon = <Search size={14} color="var(--color-icon)" />,
maxWidth = '100%',
style
}: CollapsibleSearchBarProps) => {
const [searchVisible, setSearchVisible] = useState(false)
const [searchText, setSearchText] = useState('')
const inputRef = useRef<InputRef>(null)
@ -46,16 +55,16 @@ const CollapsibleSearchBar: React.FC<CollapsibleSearchBarProps> = ({ onSearch, i
initial="collapsed"
animate={searchVisible ? 'expanded' : 'collapsed'}
variants={{
expanded: { maxWidth: maxWidth || '100%', opacity: 1, transition: { duration: 0.3, ease: 'easeInOut' } },
expanded: { maxWidth: maxWidth, opacity: 1, transition: { duration: 0.3, ease: 'easeInOut' } },
collapsed: { maxWidth: 0, opacity: 0, transition: { duration: 0.3, ease: 'easeInOut' } }
}}
style={{ overflow: 'hidden', flex: 1 }}>
<Input
ref={inputRef}
type="text"
placeholder={t('models.search')}
placeholder={placeholder}
size="small"
suffix={icon || <Search size={14} color="var(--color-icon)" />}
suffix={icon}
value={searchText}
autoFocus
allowClear
@ -71,7 +80,7 @@ const CollapsibleSearchBar: React.FC<CollapsibleSearchBarProps> = ({ onSearch, i
if (!searchText) setSearchVisible(false)
}}
onClear={handleClear}
style={{ width: '100%' }}
style={{ width: '100%', ...style }}
/>
</motion.div>
<motion.div
@ -83,8 +92,8 @@ const CollapsibleSearchBar: React.FC<CollapsibleSearchBarProps> = ({ onSearch, i
}}
style={{ cursor: 'pointer', display: 'flex' }}
onClick={() => setSearchVisible(true)}>
<Tooltip title={t('models.search')} mouseLeaveDelay={0}>
{icon || <Search size={14} color="var(--color-icon)" />}
<Tooltip title={tooltip} mouseEnterDelay={0.5} mouseLeaveDelay={0}>
{icon}
</Tooltip>
</motion.div>
</div>

View File

@ -41,7 +41,7 @@ const SelectModelSearchBar: React.FC<SelectModelSearchBarProps> = ({ onSearch })
</SearchIcon>
}
ref={inputRef}
placeholder={t('models.search')}
placeholder={t('models.search.placeholder')}
value={searchText}
onChange={(e) => handleTextChange(e.target.value)}
onClear={handleClear}

View File

@ -1544,7 +1544,10 @@
"rerank_model_not_support_provider": "Currently, the reranker model does not support this provider ({{provider}})",
"rerank_model_support_provider": "Currently, the reranker model only supports some providers ({{provider}})",
"rerank_model_tooltip": "Click the Manage button in Settings -> Model Services to add.",
"search": "Search models...",
"search": {
"placeholder": "Search models...",
"tooltip": "Search models"
},
"stream_output": "Stream output",
"type": {
"embedding": "Embedding",
@ -2912,6 +2915,10 @@
"text": "Text",
"uri": "URI"
},
"search": {
"placeholder": "Search MCP servers...",
"tooltip": "Search MCP servers"
},
"searchNpx": "Search MCP",
"serverPlural": "servers",
"serverSingular": "server",

View File

@ -1544,7 +1544,10 @@
"rerank_model_not_support_provider": "現在、並べ替えモデルはこのプロバイダー ({{provider}}) をサポートしていません。",
"rerank_model_support_provider": "現在の再順序付けモデルは、{{provider}} のみサポートしています",
"rerank_model_tooltip": "設定->モデルサービスに移動し、管理ボタンをクリックして追加します。",
"search": "モデルを検索...",
"search": {
"placeholder": "モデルを検索...",
"tooltip": "モデルを検索"
},
"stream_output": "ストリーム出力",
"type": {
"embedding": "埋め込み",
@ -2912,6 +2915,10 @@
"text": "テキスト",
"uri": "URI"
},
"search": {
"placeholder": "MCP サーバーを検索...",
"tooltip": "MCP サーバーを検索"
},
"searchNpx": "MCP を検索",
"serverPlural": "サーバー",
"serverSingular": "サーバー",

View File

@ -1544,7 +1544,10 @@
"rerank_model_not_support_provider": "В настоящее время модель переупорядочивания не поддерживает этого провайдера ({{provider}})",
"rerank_model_support_provider": "Текущая модель переупорядочивания поддерживается только некоторыми поставщиками ({{provider}})",
"rerank_model_tooltip": "В настройках -> Служба модели нажмите кнопку \"Управление\", чтобы добавить.",
"search": "Поиск моделей...",
"search": {
"placeholder": "Поиск моделей...",
"tooltip": "Поиск моделей"
},
"stream_output": "Потоковый вывод",
"type": {
"embedding": "Встраиваемые",
@ -2912,6 +2915,10 @@
"text": "Текст",
"uri": "URI"
},
"search": {
"placeholder": "Найти MCP серверы...",
"tooltip": "Найти MCP серверы"
},
"searchNpx": "Найти MCP",
"serverPlural": "серверы",
"serverSingular": "сервер",

View File

@ -1544,7 +1544,10 @@
"rerank_model_not_support_provider": "目前重排序模型不支持该服务商 ({{provider}})",
"rerank_model_support_provider": "目前重排序模型仅支持部分服务商 ({{provider}})",
"rerank_model_tooltip": "在设置 -> 模型服务中点击管理按钮添加",
"search": "搜索模型...",
"search": {
"placeholder": "搜索模型...",
"tooltip": "搜索模型"
},
"stream_output": "流式输出",
"type": {
"embedding": "嵌入",
@ -2912,6 +2915,10 @@
"text": "文本",
"uri": "URI"
},
"search": {
"placeholder": "搜索 MCP 服务器...",
"tooltip": "搜索 MCP 服务器"
},
"searchNpx": "搜索 MCP",
"serverPlural": "服务器",
"serverSingular": "服务器",

View File

@ -1544,7 +1544,10 @@
"rerank_model_not_support_provider": "目前,重新排序模型不支援此提供者({{provider}}",
"rerank_model_support_provider": "目前重排序模型僅支持部分服務商 ({{provider}})",
"rerank_model_tooltip": "在設定 -> 模型服務中點擊管理按鈕添加",
"search": "搜尋模型...",
"search": {
"placeholder": "搜尋模型...",
"tooltip": "搜尋模型"
},
"stream_output": "串流輸出",
"type": {
"embedding": "嵌入",
@ -2912,6 +2915,10 @@
"text": "文字",
"uri": "URI"
},
"search": {
"placeholder": "搜索 MCP 伺服器...",
"tooltip": "搜索 MCP 伺服器"
},
"searchNpx": "搜索 MCP",
"serverPlural": "伺服器",
"serverSingular": "伺服器",

View File

@ -1544,7 +1544,10 @@
"rerank_model_not_support_provider": "Ο επαναξιολογητικός μοντέλος δεν υποστηρίζει αυτόν τον πάροχο ({{provider}})",
"rerank_model_support_provider": "Σημειώστε ότι το μοντέλο αναδιάταξης υποστηρίζεται από μερικούς παρόχους ({{provider}})",
"rerank_model_tooltip": "Κάντε κλικ στο κουμπί Διαχείριση στο παράθυρο Ρυθμίσεις -> Υπηρεσία Μοντέλων",
"search": "Αναζήτηση μοντέλου...",
"search": {
"placeholder": "Αναζήτηση μοντέλου...",
"tooltip": "Αναζήτηση μοντέλου"
},
"stream_output": "Διαρκής Εξόδος",
"type": {
"embedding": "ενσωμάτωση",
@ -2912,6 +2915,10 @@
"text": "Κείμενο",
"uri": "URI"
},
"search": {
"placeholder": "Αναζήτηση MCP διακομιστών...",
"tooltip": "Αναζήτηση MCP διακομιστών"
},
"searchNpx": "Αναζήτηση MCP",
"serverPlural": "Διακομιστές",
"serverSingular": "Διακομιστής",

View File

@ -1544,7 +1544,10 @@
"rerank_model_not_support_provider": "Actualmente, el modelo de reordenamiento no admite este proveedor ({{provider}})",
"rerank_model_support_provider": "Actualmente, el modelo de reordenamiento solo es compatible con algunos proveedores ({{provider}})",
"rerank_model_tooltip": "Haga clic en el botón Administrar en Configuración->Servicio de modelos para agregar",
"search": "Buscar modelo...",
"search": {
"placeholder": "Buscar modelo...",
"tooltip": "Buscar modelo"
},
"stream_output": "Salida en flujo",
"type": {
"embedding": "Incrustación",
@ -2912,6 +2915,10 @@
"text": "Texto",
"uri": "URI"
},
"search": {
"placeholder": "Buscar servidores MCP...",
"tooltip": "Buscar servidores MCP"
},
"searchNpx": "Buscar MCP",
"serverPlural": "Servidores",
"serverSingular": "Servidor",

View File

@ -1544,7 +1544,10 @@
"rerank_model_not_support_provider": "Le modèle de réordonnancement ne prend pas en charge ce fournisseur ({{provider}}) pour le moment",
"rerank_model_support_provider": "Le modèle de réordonnancement ne prend actuellement en charge que certains fournisseurs ({{provider}})",
"rerank_model_tooltip": "Cliquez sur le bouton Gérer dans Paramètres -> Services de modèles pour ajouter",
"search": "Rechercher un modèle...",
"search": {
"placeholder": "Rechercher un modèle...",
"tooltip": "Rechercher un modèle"
},
"stream_output": "Sortie en flux",
"type": {
"embedding": "Incorporation",
@ -2912,6 +2915,10 @@
"text": "Текст",
"uri": "URI"
},
"search": {
"placeholder": "Rechercher des serveurs MCP...",
"tooltip": "Rechercher des serveurs MCP"
},
"searchNpx": "Поиск MCP",
"serverPlural": "Serveurs",
"serverSingular": "Serveur",

View File

@ -1544,7 +1544,10 @@
"rerank_model_not_support_provider": "Atualmente o modelo de reclassificação não suporta este provedor ({{provider}})",
"rerank_model_support_provider": "O modelo de reclassificação atualmente suporta apenas alguns provedores ({{provider}})",
"rerank_model_tooltip": "Clique no botão Gerenciar em Configurações -> Serviço de modelos para adicionar",
"search": "Procurar modelo...",
"search": {
"placeholder": "Procurar modelo...",
"tooltip": "Procurar modelo"
},
"stream_output": "Saída em fluxo",
"type": {
"embedding": "inserção",
@ -2912,6 +2915,10 @@
"text": "Texto",
"uri": "URI"
},
"search": {
"placeholder": "Buscar servidores MCP...",
"tooltip": "Buscar servidores MCP"
},
"searchNpx": "Buscar MCP",
"serverPlural": "Servidores",
"serverSingular": "Servidor",

View File

@ -1,13 +1,15 @@
import { nanoid } from '@reduxjs/toolkit'
import { Sortable } from '@renderer/components/dnd'
import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
import { Sortable, useDndReorder } from '@renderer/components/dnd'
import { EditIcon, RefreshIcon } from '@renderer/components/Icons'
import Scrollbar from '@renderer/components/Scrollbar'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { MCPServer } from '@renderer/types'
import { formatMcpError } from '@renderer/utils/error'
import { matchKeywordsInString } from '@renderer/utils/match'
import { Button, Dropdown, Empty } from 'antd'
import { Plus } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FC, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
@ -30,6 +32,32 @@ const McpServersList: FC = () => {
const [loadingServerIds, setLoadingServerIds] = useState<Set<string>>(new Set())
const [serverVersions, setServerVersions] = useState<Record<string, string | null>>({})
const [searchText, _setSearchText] = useState('')
const setSearchText = useCallback((text: string) => {
startTransition(() => {
_setSearchText(text)
})
}, [])
const filteredMcpServers = useMemo(() => {
if (!searchText.trim()) return mcpServers
const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean)
return mcpServers.filter((server) => {
const searchTarget = `${server.name} ${server.description} ${server.tags?.join(' ')}`
return matchKeywordsInString(keywords, searchTarget)
})
}, [mcpServers, searchText])
const { onSortEnd } = useDndReorder({
originalList: mcpServers,
filteredList: filteredMcpServers,
onUpdate: updateMcpServers,
idKey: 'id'
})
const scrollRef = useRef<HTMLDivElement>(null)
// 简单的滚动位置记忆
@ -190,8 +218,14 @@ const McpServersList: FC = () => {
return (
<Container ref={scrollRef}>
<ListHeader>
<SettingTitle style={{ gap: 3 }}>
<SettingTitle style={{ gap: 6 }}>
<span>{t('settings.mcp.newServer')}</span>
<CollapsibleSearchBar
onSearch={setSearchText}
placeholder={t('settings.mcp.search.placeholder')}
tooltip={t('settings.mcp.search.tooltip')}
style={{ borderRadius: 20 }}
/>
</SettingTitle>
<ButtonGroup>
<InstallNpxUv mini />
@ -213,14 +247,9 @@ const McpServersList: FC = () => {
</ButtonGroup>
</ListHeader>
<Sortable
items={mcpServers}
items={filteredMcpServers}
itemKey="id"
onSortEnd={({ oldIndex, newIndex }) => {
const newList = [...mcpServers]
const [removed] = newList.splice(oldIndex, 1)
newList.splice(newIndex, 0, removed)
updateMcpServers(newList)
}}
onSortEnd={onSortEnd}
layout="grid"
useDragOverlay
showGhost
@ -236,10 +265,10 @@ const McpServersList: FC = () => {
/>
)}
/>
{mcpServers.length === 0 && (
{(mcpServers.length === 0 || filteredMcpServers.length === 0) && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={t('settings.mcp.noServers')}
description={mcpServers.length === 0 ? t('settings.mcp.noServers') : t('common.no_results')}
style={{ marginTop: 20 }}
/>
)}

View File

@ -106,7 +106,11 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
{modelCount}
</CustomTag>
)}
<CollapsibleSearchBar onSearch={setSearchText} />
<CollapsibleSearchBar
onSearch={setSearchText}
placeholder={t('models.search.placeholder')}
tooltip={t('models.search.tooltip')}
/>
</HStack>
<HStack>
<Tooltip title={t('settings.models.check.button_caption')} mouseLeaveDelay={0}>