### 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
| @@ -13,15 +13,23 @@ export type BulkOperateItemType = { | |||
| 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) => { | |||
| return id === 'delete'; | |||
| }, []); | |||
| return ( | |||
| <Card className="mb-4"> | |||
| <Card className={cn('mb-4', className)}> | |||
| <CardContent className="p-1 pl-5 flex items-center gap-6"> | |||
| <section className="text-text-sub-title-invert flex items-center gap-2"> | |||
| <span>Selected: {count} Files</span> | |||
| @@ -66,7 +66,7 @@ export const useCreateMcpServer = () => { | |||
| queryKey: [McpApiAction.ListMcpServer], | |||
| }); | |||
| } | |||
| return data; | |||
| return data.code; | |||
| }, | |||
| }); | |||
| @@ -90,7 +90,7 @@ export const useUpdateMcpServer = () => { | |||
| queryKey: [McpApiAction.ListMcpServer], | |||
| }); | |||
| } | |||
| return data; | |||
| return data.code; | |||
| }, | |||
| }); | |||
| @@ -1,3 +1,4 @@ | |||
| import sonnerMessage from '@/components/ui/message'; | |||
| import { MessageType } from '@/constants/chat'; | |||
| import { | |||
| useHandleMessageInputChange, | |||
| @@ -14,7 +15,6 @@ import { | |||
| import { Message } from '@/interfaces/database/chat'; | |||
| import i18n from '@/locales/config'; | |||
| import api from '@/utils/api'; | |||
| import { message } from 'antd'; | |||
| import { get } from 'lodash'; | |||
| import trim from 'lodash/trim'; | |||
| import { useCallback, useContext, useEffect, useMemo } from 'react'; | |||
| @@ -28,8 +28,6 @@ import { BeginQuery } from '../interface'; | |||
| import useGraphStore from '../store'; | |||
| import { receiveMessageError } from '../utils'; | |||
| const antMessage = message; | |||
| export const useSelectNextMessages = () => { | |||
| const { data: flowDetail, loading } = useFetchAgent(); | |||
| const reference = flowDetail.dsl.retrieval; | |||
| @@ -139,7 +137,7 @@ export const useSendNextMessage = () => { | |||
| const res = await send(params); | |||
| if (receiveMessageError(res)) { | |||
| antMessage.error(res?.data?.message); | |||
| sonnerMessage.error(res?.data?.message); | |||
| // cancel loading | |||
| setValue(message.content); | |||
| @@ -465,7 +465,7 @@ export const initialCategorizeValues = { | |||
| }; | |||
| export const initialMessageValues = { | |||
| messages: [], | |||
| content: [''], | |||
| }; | |||
| export const initialKeywordExtractValues = { | |||
| @@ -1,18 +1,15 @@ | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| import { initialMessageValues } from '../../constant'; | |||
| import { convertToObjectArray } from '../../utils'; | |||
| const defaultValues = { | |||
| content: [], | |||
| }; | |||
| export function useValues(node?: RAGFlowNodeType) { | |||
| const values = useMemo(() => { | |||
| const formData = node?.data?.form; | |||
| if (isEmpty(formData)) { | |||
| return defaultValues; | |||
| return initialMessageValues; | |||
| } | |||
| return { | |||
| @@ -19,7 +19,7 @@ export function EditMcpDialog({ hideModal, loading, onOk }: IModalProps<any>) { | |||
| <DialogHeader> | |||
| <DialogTitle>Edit profile</DialogTitle> | |||
| </DialogHeader> | |||
| <EditMcpForm onOk={onOk} hideModal={hideModal}></EditMcpForm> | |||
| <EditMcpForm onOk={onOk}></EditMcpForm> | |||
| <DialogFooter> | |||
| <ButtonLoading type="submit" form={FormId} loading={loading}> | |||
| {t('common.save')} | |||
| @@ -30,7 +30,6 @@ const ServerTypeOptions = buildOptions(ServerType); | |||
| export function EditMcpForm({ | |||
| initialName, | |||
| hideModal, | |||
| onOk, | |||
| }: IModalProps<any> & { initialName?: string }) { | |||
| const { t } = useTranslation(); | |||
| @@ -61,11 +60,8 @@ export function EditMcpForm({ | |||
| 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(() => { | |||
| @@ -1,20 +1,22 @@ | |||
| import { BulkOperateBar } from '@/components/bulk-operate-bar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { SearchInput } from '@/components/ui/input'; | |||
| import { useListMcpServer } from '@/hooks/use-mcp-request'; | |||
| import { Import, Plus } from 'lucide-react'; | |||
| import { EditMcpDialog } from './edit-mcp-dialog'; | |||
| import { McpCard } from './mcp-card'; | |||
| import { useBulkOperateMCP } from './use-bulk-operate-mcp'; | |||
| import { useEditMcp } from './use-edit-mcp'; | |||
| const list = new Array(10).fill('1'); | |||
| export default function McpServer() { | |||
| const { data } = useListMcpServer(); | |||
| const { editVisible, showEditModal, hideEditModal, handleOk } = useEditMcp(); | |||
| const { list, selectedList, handleSelectChange } = useBulkOperateMCP(); | |||
| return ( | |||
| <section className="p-4"> | |||
| <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="flex gap-5"> | |||
| <SearchInput className="w-40"></SearchInput> | |||
| @@ -26,9 +28,21 @@ export default function McpServer() { | |||
| </Button> | |||
| </div> | |||
| </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) => ( | |||
| <McpCard key={item.id} data={item}></McpCard> | |||
| <McpCard | |||
| key={item.id} | |||
| data={item} | |||
| selectedList={selectedList} | |||
| handleSelectChange={handleSelectChange} | |||
| ></McpCard> | |||
| ))} | |||
| </section> | |||
| {editVisible && ( | |||
| @@ -1,39 +1,48 @@ | |||
| import { MoreButton } from '@/components/more-button'; | |||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||
| 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 { formatDate } from '@/utils/date'; | |||
| import { McpDropdown } from './mcp-dropdown'; | |||
| import { UseBulkOperateMCPReturnType } from './use-bulk-operate-mcp'; | |||
| export type DatasetCardProps = { | |||
| data: IMcpServer; | |||
| }; | |||
| export function McpCard({ data }: DatasetCardProps) { | |||
| const { navigateToAgent } = useNavigatePage(); | |||
| } & Pick<UseBulkOperateMCPReturnType, 'handleSelectChange' | 'selectedList'>; | |||
| export function McpCard({ | |||
| data, | |||
| selectedList, | |||
| handleSelectChange, | |||
| }: DatasetCardProps) { | |||
| 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"> | |||
| <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> | |||
| <McpDropdown> | |||
| <MoreButton></MoreButton> | |||
| </McpDropdown> | |||
| </section> | |||
| <div className="flex justify-between items-end"> | |||
| <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)} | |||
| </p> | |||
| </div> | |||
| @@ -0,0 +1,37 @@ | |||
| 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>; | |||
| @@ -28,13 +28,17 @@ export const useEditMcp = () => { | |||
| const handleOk = useCallback( | |||
| async (values: any) => { | |||
| let code; | |||
| if (id) { | |||
| updateMcpServer(values); | |||
| code = await updateMcpServer(values); | |||
| } else { | |||
| createMcpServer(values); | |||
| code = await createMcpServer(values); | |||
| } | |||
| if (code === 0) { | |||
| hideEditModal(); | |||
| } | |||
| }, | |||
| [createMcpServer, id, updateMcpServer], | |||
| [createMcpServer, hideEditModal, id, updateMcpServer], | |||
| ); | |||
| return { | |||
| @@ -11,7 +11,7 @@ import { convertTheKeysOfTheObjectToSnake } from './common-util'; | |||
| const FAILED_TO_FETCH = 'Failed to fetch'; | |||
| const RetcodeMessage = { | |||
| export const RetcodeMessage = { | |||
| 200: i18n.t('message.200'), | |||
| 201: i18n.t('message.201'), | |||
| 202: i18n.t('message.202'), | |||
| @@ -29,7 +29,7 @@ const RetcodeMessage = { | |||
| 503: i18n.t('message.503'), | |||
| 504: i18n.t('message.504'), | |||
| }; | |||
| type ResultCode = | |||
| export type ResultCode = | |||
| | 200 | |||
| | 201 | |||
| | 202 | |||