From dc068bbd1eaff7aaf16d9a07d9063131c99e3bbe Mon Sep 17 00:00:00 2001 From: balibabu Date: Mon, 14 Jul 2025 11:46:52 +0800 Subject: [PATCH] Feat: Filter MCP server list by text. #3221 (#8820) ### What problem does this PR solve? Feat: Filter MCP server list by text. #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/hooks/use-mcp-request.ts | 40 ++++++++-- web/src/locales/en.ts | 2 + .../profile-setting/mcp/edit-mcp-dialog.tsx | 76 ++++++++++++------- .../profile-setting/mcp/edit-mcp-form.tsx | 22 +++++- web/src/pages/profile-setting/mcp/index.tsx | 30 +++++++- .../pages/profile-setting/mcp/use-edit-mcp.ts | 6 +- web/src/services/mcp-server-service.ts | 4 + 7 files changed, 136 insertions(+), 44 deletions(-) diff --git a/web/src/hooks/use-mcp-request.ts b/web/src/hooks/use-mcp-request.ts index 9aa8c3a21..ac0922243 100644 --- a/web/src/hooks/use-mcp-request.ts +++ b/web/src/hooks/use-mcp-request.ts @@ -11,8 +11,15 @@ import { ITestMcpRequestBody, } from '@/interfaces/request/mcp'; import i18n from '@/locales/config'; -import mcpServerService from '@/services/mcp-server-service'; +import mcpServerService, { + listMcpServers, +} from '@/services/mcp-server-service'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useDebounce } from 'ahooks'; +import { + useGetPaginationWithRouter, + useHandleSearchChange, +} from './logic-hooks'; export const enum McpApiAction { ListMcpServer = 'listMcpServer', @@ -29,17 +36,38 @@ export const enum McpApiAction { } export const useListMcpServer = () => { + const { searchString, handleInputChange } = useHandleSearchChange(); + const { pagination, setPagination } = useGetPaginationWithRouter(); + const debouncedSearchString = useDebounce(searchString, { wait: 500 }); + const { data, isFetching: loading } = useQuery({ - queryKey: [McpApiAction.ListMcpServer], + queryKey: [ + McpApiAction.ListMcpServer, + { + debouncedSearchString, + ...pagination, + }, + ], initialData: { total: 0, mcp_servers: [] }, gcTime: 0, queryFn: async () => { - const { data } = await mcpServerService.list({}); + const { data } = await listMcpServers({ + keywords: debouncedSearchString, + page_size: pagination.pageSize, + page: pagination.current, + }); return data?.data; }, }); - return { data, loading }; + return { + data, + loading, + handleInputChange, + setPagination, + searchString, + pagination: { ...pagination, total: data?.total }, + }; }; export const useGetMcpServer = (id: string) => { @@ -191,12 +219,12 @@ export const useTestMcpServer = () => { data, isPending: loading, mutateAsync, - } = useMutation({ + } = useMutation, Error, ITestMcpRequestBody>({ mutationKey: [McpApiAction.TestMcpServer], mutationFn: async (params) => { const { data } = await mcpServerService.test(params); - return data?.data || []; + return data; }, }); diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 33538a509..58e3d921d 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1307,6 +1307,8 @@ This delimiter is used to split the input text into several text pieces echo of export: 'Export', import: 'Import', addMcp: 'Add MCP', + url: 'URL', + serverType: 'Server Type', }, }, }; diff --git a/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx b/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx index d40578ccd..37b52dfdf 100644 --- a/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx +++ b/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx @@ -10,10 +10,17 @@ import { import { useGetMcpServer, useTestMcpServer } from '@/hooks/use-mcp-request'; import { IModalProps } from '@/interfaces/common'; import { IMCPTool, IMCPToolObject } from '@/interfaces/database/mcp'; +import { cn } from '@/lib/utils'; import { zodResolver } from '@hookform/resolvers/zod'; -import { isEmpty, omit, pick } from 'lodash'; +import { isEmpty, pick } from 'lodash'; import { RefreshCw } from 'lucide-react'; -import { MouseEventHandler, useCallback, useMemo, useState } from 'react'; +import { + MouseEventHandler, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; @@ -25,13 +32,6 @@ import { } from './edit-mcp-form'; import { McpToolCard } from './tool-card'; -function transferToolToObject(tools: IMCPTool[] = []) { - return tools.reduce((pre, tool) => { - pre[tool.name] = omit(tool, 'name'); - return pre; - }, {}); -} - function transferToolToArray(tools: IMCPToolObject) { return Object.entries(tools).reduce((pre, [name, tool]) => { pre.push({ ...tool, name }); @@ -39,6 +39,12 @@ function transferToolToArray(tools: IMCPToolObject) { }, []); } +const DefaultValues = { + name: '', + server_type: ServerType.SSE, + url: '', +}; + export function EditMcpDialog({ hideModal, loading, @@ -48,19 +54,22 @@ export function EditMcpDialog({ const { t } = useTranslation(); const { testMcpServer, - data: tools, + data: testData, loading: testLoading, } = useTestMcpServer(); const [isTriggeredBySaving, setIsTriggeredBySaving] = useState(false); const FormSchema = useBuildFormSchema(); const [collapseOpen, setCollapseOpen] = useState(true); const { data } = useGetMcpServer(id); + const [fieldChanged, setFieldChanged] = useState(false); + + const tools = useMemo(() => { + return testData?.data || []; + }, [testData?.data]); const form = useForm>({ resolver: zodResolver(FormSchema), - values: isEmpty(data) - ? { name: '', server_type: ServerType.SSE, url: '' } - : pick(data, ['name', 'server_type', 'url']), + defaultValues: DefaultValues, }); const handleTest: MouseEventHandler = useCallback((e) => { @@ -74,35 +83,42 @@ export function EditMcpDialog({ const handleOk = async (values: z.infer) => { if (isTriggeredBySaving) { - onOk?.({ - ...values, - variables: { - ...(values?.variables || {}), - tools: transferToolToObject(tools), - }, - }); + onOk?.(values); } else { - testMcpServer(values); + const ret = await testMcpServer(values); + if (ret.code === 0) { + setFieldChanged(false); + } } }; + useEffect(() => { + if (!isEmpty(data)) { + form.reset(pick(data, ['name', 'server_type', 'url'])); + } + }, [data, form]); + const nextTools = useMemo(() => { - return tools || transferToolToArray(data.variables?.tools || {}); + return isEmpty(tools) + ? transferToolToArray(data.variables?.tools || {}) + : tools; }, [data.variables?.tools, tools]); - const dirtyFields = form.formState.dirtyFields; - const fieldChanged = 'server_type' in dirtyFields || 'url' in dirtyFields; const disabled = !!!tools?.length || testLoading || fieldChanged; return ( - Edit profile + {t('common.edit')} - + {tools?.length || 0} tools available} + title={
{nextTools?.length || 0} tools available
} open={collapseOpen} onOpenChange={setCollapseOpen} rightContent={ @@ -112,7 +128,11 @@ export function EditMcpDialog({ type="submit" onClick={handleTest} > - + } > diff --git a/web/src/pages/profile-setting/mcp/edit-mcp-form.tsx b/web/src/pages/profile-setting/mcp/edit-mcp-form.tsx index 0e063743c..ceb2b0cf8 100644 --- a/web/src/pages/profile-setting/mcp/edit-mcp-form.tsx +++ b/web/src/pages/profile-setting/mcp/edit-mcp-form.tsx @@ -15,6 +15,7 @@ import { Input } from '@/components/ui/input'; import { RAGFlowSelect } from '@/components/ui/select'; import { IModalProps } from '@/interfaces/common'; import { buildOptions } from '@/utils/form'; +import { Dispatch, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; export const FormId = 'EditMcpForm'; @@ -38,6 +39,7 @@ export function useBuildFormSchema() { .trim(), url: z .string() + .url() .min(1, { message: t('common.namePlaceholder'), }) @@ -48,7 +50,7 @@ export function useBuildFormSchema() { message: t('common.namePlaceholder'), }) .trim(), - variables: z.object({}).optional(), + // variables: z.object({}).optional(), }); return FormSchema; @@ -57,7 +59,11 @@ export function useBuildFormSchema() { export function EditMcpForm({ form, onOk, -}: IModalProps & { form: UseFormReturn }) { + setFieldChanged, +}: IModalProps & { + form: UseFormReturn; + setFieldChanged: Dispatch>; +}) { const { t } = useTranslation(); const FormSchema = useBuildFormSchema(); @@ -94,12 +100,16 @@ export function EditMcpForm({ name="url" render={({ field }) => ( - {t('common.url')} + {t('mcp.url')} { + field.onChange(e.target.value.trim()); + setFieldChanged(true); + }} /> @@ -111,12 +121,16 @@ export function EditMcpForm({ name="server_type" render={({ field }) => ( - {t('common.serverType')} + {t('mcp.serverType')} { + field.onChange(value); + setFieldChanged(true); + }} /> diff --git a/web/src/pages/profile-setting/mcp/index.tsx b/web/src/pages/profile-setting/mcp/index.tsx index 665f8ad07..b06a0eae9 100644 --- a/web/src/pages/profile-setting/mcp/index.tsx +++ b/web/src/pages/profile-setting/mcp/index.tsx @@ -1,8 +1,11 @@ import { BulkOperateBar } from '@/components/bulk-operate-bar'; import { Button } from '@/components/ui/button'; import { SearchInput } from '@/components/ui/input'; +import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; import { useListMcpServer } from '@/hooks/use-mcp-request'; +import { pick } from 'lodash'; import { Import, Plus } from 'lucide-react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { EditMcpDialog } from './edit-mcp-dialog'; import { ImportMcpDialog } from './import-mcp-dialog'; @@ -12,14 +15,22 @@ import { useEditMcp } from './use-edit-mcp'; import { useImportMcp } from './use-import-mcp'; export default function McpServer() { - const { data } = useListMcpServer(); - const { editVisible, showEditModal, hideEditModal, handleOk, id } = + const { data, setPagination, searchString, handleInputChange, pagination } = + useListMcpServer(); + const { editVisible, showEditModal, hideEditModal, handleOk, id, loading } = useEditMcp(); const { list, selectedList, handleSelectChange } = useBulkOperateMCP(); const { t } = useTranslation(); const { importVisible, showImportModal, hideImportModal, onImportOk } = useImportMcp(); + const handlePageChange = useCallback( + (page: number, pageSize?: number) => { + setPagination({ page, pageSize }); + }, + [setPagination], + ); + return (
MCP Servers
@@ -28,7 +39,11 @@ export default function McpServer() { Customize the list of MCP servers
- + @@ -37,6 +52,7 @@ export default function McpServer() {
+ {selectedList.length > 0 && ( ))} +
+ +
{editVisible && ( )} {importVisible && ( diff --git a/web/src/pages/profile-setting/mcp/use-edit-mcp.ts b/web/src/pages/profile-setting/mcp/use-edit-mcp.ts index 94895d935..09fc83be5 100644 --- a/web/src/pages/profile-setting/mcp/use-edit-mcp.ts +++ b/web/src/pages/profile-setting/mcp/use-edit-mcp.ts @@ -14,7 +14,7 @@ export const useEditMcp = () => { const { createMcpServer, loading } = useCreateMcpServer(); const [id, setId] = useState(''); - const { updateMcpServer } = useUpdateMcpServer(); + const { updateMcpServer, loading: updateLoading } = useUpdateMcpServer(); const handleShowModal = useCallback( (id: string) => () => { @@ -28,7 +28,7 @@ export const useEditMcp = () => { async (values: any) => { let code; if (id) { - code = await updateMcpServer(values); + code = await updateMcpServer({ ...values, mcp_id: id }); } else { code = await createMcpServer(values); } @@ -43,7 +43,7 @@ export const useEditMcp = () => { editVisible, hideEditModal, showEditModal: handleShowModal, - loading, + loading: loading || updateLoading, createMcpServer, handleOk, id, diff --git a/web/src/services/mcp-server-service.ts b/web/src/services/mcp-server-service.ts index 7cb22a090..3bad79325 100644 --- a/web/src/services/mcp-server-service.ts +++ b/web/src/services/mcp-server-service.ts @@ -1,3 +1,4 @@ +import { IPaginationRequestBody } from '@/interfaces/request/base'; import api from '@/utils/api'; import registerServer from '@/utils/register-server'; import request from '@/utils/request'; @@ -66,3 +67,6 @@ const methods = { const mcpServerService = registerServer(methods, request); export default mcpServerService; + +export const listMcpServers = (params?: IPaginationRequestBody, body?: any) => + request.post(api.listMcpServer, { data: body || {}, params });