### What problem does this PR solve? Feat: Display MCP multiple selection bar #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| onClick(): void; | onClick(): void; | ||||
| }; | }; | ||||
| type BulkOperateBarProps = { list: BulkOperateItemType[]; count: number }; | |||||
| type BulkOperateBarProps = { | |||||
| list: BulkOperateItemType[]; | |||||
| count: number; | |||||
| className?: string; | |||||
| }; | |||||
| export function BulkOperateBar({ list, count }: BulkOperateBarProps) { | |||||
| export function BulkOperateBar({ | |||||
| list, | |||||
| count, | |||||
| className, | |||||
| }: BulkOperateBarProps) { | |||||
| const isDeleteItem = useCallback((id: string) => { | const isDeleteItem = useCallback((id: string) => { | ||||
| return id === 'delete'; | return id === 'delete'; | ||||
| }, []); | }, []); | ||||
| return ( | return ( | ||||
| <Card className="mb-4"> | |||||
| <Card className={cn('mb-4', className)}> | |||||
| <CardContent className="p-1 pl-5 flex items-center gap-6"> | <CardContent className="p-1 pl-5 flex items-center gap-6"> | ||||
| <section className="text-text-sub-title-invert flex items-center gap-2"> | <section className="text-text-sub-title-invert flex items-center gap-2"> | ||||
| <span>Selected: {count} Files</span> | <span>Selected: {count} Files</span> |
| queryKey: [McpApiAction.ListMcpServer], | queryKey: [McpApiAction.ListMcpServer], | ||||
| }); | }); | ||||
| } | } | ||||
| return data; | |||||
| return data.code; | |||||
| }, | }, | ||||
| }); | }); | ||||
| queryKey: [McpApiAction.ListMcpServer], | queryKey: [McpApiAction.ListMcpServer], | ||||
| }); | }); | ||||
| } | } | ||||
| return data; | |||||
| return data.code; | |||||
| }, | }, | ||||
| }); | }); | ||||
| import sonnerMessage from '@/components/ui/message'; | |||||
| import { MessageType } from '@/constants/chat'; | import { MessageType } from '@/constants/chat'; | ||||
| import { | import { | ||||
| useHandleMessageInputChange, | useHandleMessageInputChange, | ||||
| import { Message } from '@/interfaces/database/chat'; | import { Message } from '@/interfaces/database/chat'; | ||||
| import i18n from '@/locales/config'; | import i18n from '@/locales/config'; | ||||
| import api from '@/utils/api'; | import api from '@/utils/api'; | ||||
| import { message } from 'antd'; | |||||
| import { get } from 'lodash'; | import { get } from 'lodash'; | ||||
| import trim from 'lodash/trim'; | import trim from 'lodash/trim'; | ||||
| import { useCallback, useContext, useEffect, useMemo } from 'react'; | import { useCallback, useContext, useEffect, useMemo } from 'react'; | ||||
| import useGraphStore from '../store'; | import useGraphStore from '../store'; | ||||
| import { receiveMessageError } from '../utils'; | import { receiveMessageError } from '../utils'; | ||||
| const antMessage = message; | |||||
| export const useSelectNextMessages = () => { | export const useSelectNextMessages = () => { | ||||
| const { data: flowDetail, loading } = useFetchAgent(); | const { data: flowDetail, loading } = useFetchAgent(); | ||||
| const reference = flowDetail.dsl.retrieval; | const reference = flowDetail.dsl.retrieval; | ||||
| const res = await send(params); | const res = await send(params); | ||||
| if (receiveMessageError(res)) { | if (receiveMessageError(res)) { | ||||
| antMessage.error(res?.data?.message); | |||||
| sonnerMessage.error(res?.data?.message); | |||||
| // cancel loading | // cancel loading | ||||
| setValue(message.content); | setValue(message.content); |
| }; | }; | ||||
| export const initialMessageValues = { | export const initialMessageValues = { | ||||
| messages: [], | |||||
| content: [''], | |||||
| }; | }; | ||||
| export const initialKeywordExtractValues = { | export const initialKeywordExtractValues = { |
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | import { RAGFlowNodeType } from '@/interfaces/database/flow'; | ||||
| import { isEmpty } from 'lodash'; | import { isEmpty } from 'lodash'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import { initialMessageValues } from '../../constant'; | |||||
| import { convertToObjectArray } from '../../utils'; | import { convertToObjectArray } from '../../utils'; | ||||
| const defaultValues = { | |||||
| content: [], | |||||
| }; | |||||
| export function useValues(node?: RAGFlowNodeType) { | export function useValues(node?: RAGFlowNodeType) { | ||||
| const values = useMemo(() => { | const values = useMemo(() => { | ||||
| const formData = node?.data?.form; | const formData = node?.data?.form; | ||||
| if (isEmpty(formData)) { | if (isEmpty(formData)) { | ||||
| return defaultValues; | |||||
| return initialMessageValues; | |||||
| } | } | ||||
| return { | return { |
| <DialogHeader> | <DialogHeader> | ||||
| <DialogTitle>Edit profile</DialogTitle> | <DialogTitle>Edit profile</DialogTitle> | ||||
| </DialogHeader> | </DialogHeader> | ||||
| <EditMcpForm onOk={onOk} hideModal={hideModal}></EditMcpForm> | |||||
| <EditMcpForm onOk={onOk}></EditMcpForm> | |||||
| <DialogFooter> | <DialogFooter> | ||||
| <ButtonLoading type="submit" form={FormId} loading={loading}> | <ButtonLoading type="submit" form={FormId} loading={loading}> | ||||
| {t('common.save')} | {t('common.save')} |
| export function EditMcpForm({ | export function EditMcpForm({ | ||||
| initialName, | initialName, | ||||
| hideModal, | |||||
| onOk, | onOk, | ||||
| }: IModalProps<any> & { initialName?: string }) { | }: IModalProps<any> & { initialName?: string }) { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| defaultValues: { name: '', server_type: ServerType.SSE, url: '' }, | defaultValues: { name: '', server_type: ServerType.SSE, url: '' }, | ||||
| }); | }); | ||||
| async function onSubmit(data: z.infer<typeof FormSchema>) { | |||||
| const ret = await onOk?.(data); | |||||
| if (ret) { | |||||
| hideModal?.(); | |||||
| } | |||||
| function onSubmit(data: z.infer<typeof FormSchema>) { | |||||
| onOk?.(data); | |||||
| } | } | ||||
| useEffect(() => { | useEffect(() => { |
| import { BulkOperateBar } from '@/components/bulk-operate-bar'; | |||||
| import { Button } from '@/components/ui/button'; | import { Button } from '@/components/ui/button'; | ||||
| import { SearchInput } from '@/components/ui/input'; | import { SearchInput } from '@/components/ui/input'; | ||||
| import { useListMcpServer } from '@/hooks/use-mcp-request'; | import { useListMcpServer } from '@/hooks/use-mcp-request'; | ||||
| import { Import, Plus } from 'lucide-react'; | import { Import, Plus } from 'lucide-react'; | ||||
| import { EditMcpDialog } from './edit-mcp-dialog'; | import { EditMcpDialog } from './edit-mcp-dialog'; | ||||
| import { McpCard } from './mcp-card'; | import { McpCard } from './mcp-card'; | ||||
| import { useBulkOperateMCP } from './use-bulk-operate-mcp'; | |||||
| import { useEditMcp } from './use-edit-mcp'; | import { useEditMcp } from './use-edit-mcp'; | ||||
| const list = new Array(10).fill('1'); | |||||
| export default function McpServer() { | export default function McpServer() { | ||||
| const { data } = useListMcpServer(); | const { data } = useListMcpServer(); | ||||
| const { editVisible, showEditModal, hideEditModal, handleOk } = useEditMcp(); | const { editVisible, showEditModal, hideEditModal, handleOk } = useEditMcp(); | ||||
| const { list, selectedList, handleSelectChange } = useBulkOperateMCP(); | |||||
| return ( | return ( | ||||
| <section className="p-4"> | <section className="p-4"> | ||||
| <div className="text-text-title text-2xl">MCP Servers</div> | <div className="text-text-title text-2xl">MCP Servers</div> | ||||
| <section className="flex items-center justify-between"> | |||||
| <section className="flex items-center justify-between pb-5"> | |||||
| <div className="text-text-sub-title">自定义 MCP Server 的列表</div> | <div className="text-text-sub-title">自定义 MCP Server 的列表</div> | ||||
| <div className="flex gap-5"> | <div className="flex gap-5"> | ||||
| <SearchInput className="w-40"></SearchInput> | <SearchInput className="w-40"></SearchInput> | ||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| </section> | </section> | ||||
| <section className="flex gap-5 flex-wrap pt-5"> | |||||
| {selectedList.length > 0 && ( | |||||
| <BulkOperateBar | |||||
| list={list} | |||||
| count={selectedList.length} | |||||
| className="mb-2.5" | |||||
| ></BulkOperateBar> | |||||
| )} | |||||
| <section className="flex gap-5 flex-wrap"> | |||||
| {data.mcp_servers.map((item) => ( | {data.mcp_servers.map((item) => ( | ||||
| <McpCard key={item.id} data={item}></McpCard> | |||||
| <McpCard | |||||
| key={item.id} | |||||
| data={item} | |||||
| selectedList={selectedList} | |||||
| handleSelectChange={handleSelectChange} | |||||
| ></McpCard> | |||||
| ))} | ))} | ||||
| </section> | </section> | ||||
| {editVisible && ( | {editVisible && ( |
| import { MoreButton } from '@/components/more-button'; | import { MoreButton } from '@/components/more-button'; | ||||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||||
| import { Card, CardContent } from '@/components/ui/card'; | import { Card, CardContent } from '@/components/ui/card'; | ||||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||||
| import { Checkbox } from '@/components/ui/checkbox'; | |||||
| import { IMcpServer } from '@/interfaces/database/mcp'; | import { IMcpServer } from '@/interfaces/database/mcp'; | ||||
| import { formatDate } from '@/utils/date'; | import { formatDate } from '@/utils/date'; | ||||
| import { McpDropdown } from './mcp-dropdown'; | import { McpDropdown } from './mcp-dropdown'; | ||||
| import { UseBulkOperateMCPReturnType } from './use-bulk-operate-mcp'; | |||||
| export type DatasetCardProps = { | export type DatasetCardProps = { | ||||
| data: IMcpServer; | data: IMcpServer; | ||||
| }; | |||||
| export function McpCard({ data }: DatasetCardProps) { | |||||
| const { navigateToAgent } = useNavigatePage(); | |||||
| } & Pick<UseBulkOperateMCPReturnType, 'handleSelectChange' | 'selectedList'>; | |||||
| export function McpCard({ | |||||
| data, | |||||
| selectedList, | |||||
| handleSelectChange, | |||||
| }: DatasetCardProps) { | |||||
| return ( | return ( | ||||
| <Card key={data.id} className="w-64" onClick={navigateToAgent(data.id)}> | |||||
| <Card key={data.id} className="w-64"> | |||||
| <CardContent className="p-2.5 pt-2 group"> | <CardContent className="p-2.5 pt-2 group"> | ||||
| <section className="flex justify-between mb-2"> | |||||
| <div className="flex gap-2 items-center"> | |||||
| <Avatar className="size-6 rounded-lg"> | |||||
| <AvatarImage src={data?.avatar} /> | |||||
| <AvatarFallback className="rounded-lg ">CN</AvatarFallback> | |||||
| </Avatar> | |||||
| <section className="flex justify-between pb-2"> | |||||
| <h3 className="text-lg font-semibold line-clamp-1">{data.name}</h3> | |||||
| <div className="space-x-4"> | |||||
| <McpDropdown> | |||||
| <MoreButton></MoreButton> | |||||
| </McpDropdown> | |||||
| <Checkbox | |||||
| checked={selectedList.includes(data.id)} | |||||
| onCheckedChange={(checked) => { | |||||
| if (typeof checked === 'boolean') { | |||||
| handleSelectChange(data.id, checked); | |||||
| } | |||||
| }} | |||||
| onClick={(e) => { | |||||
| e.stopPropagation(); | |||||
| }} | |||||
| /> | |||||
| </div> | </div> | ||||
| <McpDropdown> | |||||
| <MoreButton></MoreButton> | |||||
| </McpDropdown> | |||||
| </section> | </section> | ||||
| <div className="flex justify-between items-end"> | <div className="flex justify-between items-end"> | ||||
| <div className="w-full"> | <div className="w-full"> | ||||
| <h3 className="text-lg font-semibold mb-2 line-clamp-1"> | |||||
| {data.name} | |||||
| </h3> | |||||
| <p className="text-xs text-text-sub-title">{data.description}</p> | |||||
| <p className="text-xs text-text-sub-title"> | |||||
| <div className="text-base font-semibold mb-3 line-clamp-1 text-text-sub-title"> | |||||
| 20 cached tools | |||||
| </div> | |||||
| <p className="text-sm text-text-sub-title"> | |||||
| {formatDate(data.update_date)} | {formatDate(data.update_date)} | ||||
| </p> | </p> | ||||
| </div> | </div> |
| import { Trash2, Upload } from 'lucide-react'; | |||||
| import { useCallback, useState } from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| export function useBulkOperateMCP() { | |||||
| const { t } = useTranslation(); | |||||
| const [selectedList, setSelectedList] = useState<Array<string>>([]); | |||||
| const handleEnableClick = useCallback(() => {}, []); | |||||
| const handleDelete = useCallback(() => {}, []); | |||||
| const handleSelectChange = useCallback((id: string, checked: boolean) => { | |||||
| setSelectedList((list) => { | |||||
| return checked ? [...list, id] : list.filter((item) => item !== id); | |||||
| }); | |||||
| }, []); | |||||
| const list = [ | |||||
| { | |||||
| id: 'export', | |||||
| label: t('mcp.export'), | |||||
| icon: <Upload />, | |||||
| onClick: handleEnableClick, | |||||
| }, | |||||
| { | |||||
| id: 'delete', | |||||
| label: t('common.delete'), | |||||
| icon: <Trash2 />, | |||||
| onClick: handleDelete, | |||||
| }, | |||||
| ]; | |||||
| return { list, selectedList, handleSelectChange }; | |||||
| } | |||||
| export type UseBulkOperateMCPReturnType = ReturnType<typeof useBulkOperateMCP>; |
| const handleOk = useCallback( | const handleOk = useCallback( | ||||
| async (values: any) => { | async (values: any) => { | ||||
| let code; | |||||
| if (id) { | if (id) { | ||||
| updateMcpServer(values); | |||||
| code = await updateMcpServer(values); | |||||
| } else { | } else { | ||||
| createMcpServer(values); | |||||
| code = await createMcpServer(values); | |||||
| } | |||||
| if (code === 0) { | |||||
| hideEditModal(); | |||||
| } | } | ||||
| }, | }, | ||||
| [createMcpServer, id, updateMcpServer], | |||||
| [createMcpServer, hideEditModal, id, updateMcpServer], | |||||
| ); | ); | ||||
| return { | return { |
| const FAILED_TO_FETCH = 'Failed to fetch'; | const FAILED_TO_FETCH = 'Failed to fetch'; | ||||
| const RetcodeMessage = { | |||||
| export const RetcodeMessage = { | |||||
| 200: i18n.t('message.200'), | 200: i18n.t('message.200'), | ||||
| 201: i18n.t('message.201'), | 201: i18n.t('message.201'), | ||||
| 202: i18n.t('message.202'), | 202: i18n.t('message.202'), | ||||
| 503: i18n.t('message.503'), | 503: i18n.t('message.503'), | ||||
| 504: i18n.t('message.504'), | 504: i18n.t('message.504'), | ||||
| }; | }; | ||||
| type ResultCode = | |||||
| export type ResultCode = | |||||
| | 200 | | 200 | ||||
| | 201 | | 201 | ||||
| | 202 | | 202 |