From 1bb70f9af93c05813d36001a39202c44af55810d Mon Sep 17 00:00:00 2001 From: jZonG Date: Thu, 8 May 2025 19:36:12 +0800 Subject: [PATCH] edit & delete --- .../components/tools/mcp/detail/content.tsx | 93 ++++++++++++++-- .../tools/mcp/detail/operation-dropdown.tsx | 88 +++++++++++++++ web/app/components/tools/mcp/index.tsx | 1 + web/app/components/tools/mcp/modal.tsx | 2 +- .../components/tools/mcp/provider-card.tsx | 100 ++++++++++++++++-- web/i18n/en-US/tools.ts | 7 ++ web/i18n/zh-Hans/tools.ts | 7 ++ web/service/use-tools.ts | 57 ++++++++-- 8 files changed, 325 insertions(+), 30 deletions(-) create mode 100644 web/app/components/tools/mcp/detail/operation-dropdown.tsx diff --git a/web/app/components/tools/mcp/detail/content.tsx b/web/app/components/tools/mcp/detail/content.tsx index 5ae9370b2f..7efe997af4 100644 --- a/web/app/components/tools/mcp/detail/content.tsx +++ b/web/app/components/tools/mcp/detail/content.tsx @@ -1,6 +1,7 @@ 'use client' -import React from 'react' +import React, { useCallback } from 'react' import type { FC } from 'react' +import { useBoolean } from 'ahooks' import { useTranslation } from 'react-i18next' import { useAppContext } from '@/context/app-context' import { @@ -10,24 +11,74 @@ import type { ToolWithProvider } from '../../../workflow/types' import Icon from '@/app/components/plugins/card/base/card-icon' import ActionButton from '@/app/components/base/action-button' import Button from '@/app/components/base/button' -// import Toast from '@/app/components/base/toast' +import Confirm from '@/app/components/base/confirm' import Indicator from '@/app/components/header/indicator' +import MCPModal from '../modal' +import OperationDropdown from './operation-dropdown' +import { useDeleteMCP, useUpdateMCP } from '@/service/use-tools' import cn from '@/utils/classnames' type Props = { detail?: ToolWithProvider - onUpdate: () => void + onUpdate: (isDelete?: boolean) => void onHide: () => void } const MCPDetailContent: FC = ({ detail, - // onUpdate, + onUpdate, onHide, }) => { const { t } = useTranslation() const { isCurrentWorkspaceManager } = useAppContext() + const { mutate: updateMCP } = useUpdateMCP({ + onSuccess: onUpdate, + }) + const { mutate: deleteMCP } = useDeleteMCP({ + onSuccess: onUpdate, + }) + + const [isShowUpdateModal, { + setTrue: showUpdateModal, + setFalse: hideUpdateModal, + }] = useBoolean(false) + + const [isShowDeleteConfirm, { + setTrue: showDeleteConfirm, + setFalse: hideDeleteConfirm, + }] = useBoolean(false) + + const [deleting, { + setTrue: showDeleting, + setFalse: hideDeleting, + }] = useBoolean(false) + + const handleUpdate = useCallback(async (data: any) => { + if (!detail) + return + const res = await updateMCP({ + ...data, + provider_id: detail.id, + }) + if ((res as any)?.result === 'success') { + hideUpdateModal() + onUpdate() + } + }, [detail, updateMCP, hideUpdateModal, onUpdate]) + + const handleDelete = useCallback(async () => { + if (!detail) + return + showDeleting() + const res = await deleteMCP(detail.id) + hideDeleting() + if ((res as any)?.result === 'success') { + hideDeleteConfirm() + onUpdate(true) + } + }, [detail, showDeleting, hideDeleting, hideDeleteConfirm, onUpdate]) + if (!detail) return null @@ -45,13 +96,10 @@ const MCPDetailContent: FC = ({
{detail.server_url}
- {/* */} + /> @@ -69,7 +117,7 @@ const MCPDetailContent: FC = ({ {t('tools.auth.authorized')} )} - {detail.is_team_authorization && ( + {!detail.is_team_authorization && (
+ } + onCancel={hideDeleteConfirm} + onConfirm={handleDelete} + isLoading={deleting} + isDisabled={deleting} + /> + )} ) } diff --git a/web/app/components/tools/mcp/detail/operation-dropdown.tsx b/web/app/components/tools/mcp/detail/operation-dropdown.tsx new file mode 100644 index 0000000000..d2cbc8825d --- /dev/null +++ b/web/app/components/tools/mcp/detail/operation-dropdown.tsx @@ -0,0 +1,88 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiDeleteBinLine, + RiEditLine, + RiMoreFill, +} from '@remixicon/react' +import ActionButton from '@/app/components/base/action-button' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import cn from '@/utils/classnames' + +type Props = { + inCard?: boolean + onOpenChange?: (open: boolean) => void + onEdit: () => void + onRemove: () => void +} + +const OperationDropdown: FC = ({ + inCard, + onOpenChange, + onEdit, + onRemove, +}) => { + const { t } = useTranslation() + const [open, doSetOpen] = useState(false) + const openRef = useRef(open) + const setOpen = useCallback((v: boolean) => { + doSetOpen(v) + openRef.current = v + onOpenChange?.(v) + }, [doSetOpen]) + + const handleTrigger = useCallback(() => { + setOpen(!openRef.current) + }, [setOpen]) + + return ( + + +
+ + + +
+
+ +
+
{ + onEdit() + handleTrigger() + }} + > + +
{t('tools.mcp.operation.edit')}
+
+
{ + onRemove() + handleTrigger() + }} + > + +
{t('tools.mcp.operation.remove')}
+
+
+
+
+ ) +} +export default React.memo(OperationDropdown) diff --git a/web/app/components/tools/mcp/index.tsx b/web/app/components/tools/mcp/index.tsx index be8421e2f0..08a9f177be 100644 --- a/web/app/components/tools/mcp/index.tsx +++ b/web/app/components/tools/mcp/index.tsx @@ -60,6 +60,7 @@ const MCPList = ({ data={provider} currentProvider={currentProvider} handleSelect={setCurrentProvider} + onUpdate={() => invalidateMCPList()} /> ))} {!list.length && renderDefaultCard()} diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index d1a652de04..8edae8a2a5 100644 --- a/web/app/components/tools/mcp/modal.tsx +++ b/web/app/components/tools/mcp/modal.tsx @@ -109,7 +109,7 @@ const MCPModal = ({
- +
diff --git a/web/app/components/tools/mcp/provider-card.tsx b/web/app/components/tools/mcp/provider-card.tsx index 0ca5e3e205..72e6b56241 100644 --- a/web/app/components/tools/mcp/provider-card.tsx +++ b/web/app/components/tools/mcp/provider-card.tsx @@ -1,43 +1,90 @@ 'use client' -// import { useMemo, useState } from 'react' +import { useCallback, useState } from 'react' +import { useBoolean } from 'ahooks' import { useTranslation } from 'react-i18next' -// import { useContext } from 'use-context-selector' -// import I18n from '@/context/i18n' -// import { getLanguage } from '@/i18n/language' -// import { useAppContext } from '@/context/app-context' +import { useAppContext } from '@/context/app-context' import { RiHammerFill } from '@remixicon/react' import Indicator from '@/app/components/header/indicator' import Icon from '@/app/components/plugins/card/base/card-icon' import { useFormatTimeFromNow } from './hooks' import type { ToolWithProvider } from '../../workflow/types' +import Confirm from '@/app/components/base/confirm' +import MCPModal from './modal' +import OperationDropdown from './detail/operation-dropdown' +import { useDeleteMCP, useUpdateMCP } from '@/service/use-tools' import cn from '@/utils/classnames' type Props = { currentProvider?: ToolWithProvider data: ToolWithProvider handleSelect: (provider: ToolWithProvider) => void + onUpdate: () => void } const MCPCard = ({ currentProvider, data, + onUpdate, handleSelect, }: Props) => { const { t } = useTranslation() const { formatTimeFromNow } = useFormatTimeFromNow() - // const { locale } = useContext(I18n) - // const language = getLanguage(locale) - // const { isCurrentWorkspaceManager } = useAppContext() + const { isCurrentWorkspaceManager } = useAppContext() + + const { mutate: updateMCP } = useUpdateMCP({ + onSuccess: onUpdate, + }) + const { mutate: deleteMCP } = useDeleteMCP({ + onSuccess: onUpdate, + }) + + const [isOperationShow, setIsOperationShow] = useState(false) + + const [isShowUpdateModal, { + setTrue: showUpdateModal, + setFalse: hideUpdateModal, + }] = useBoolean(false) + + const [isShowDeleteConfirm, { + setTrue: showDeleteConfirm, + setFalse: hideDeleteConfirm, + }] = useBoolean(false) + + const [deleting, { + setTrue: showDeleting, + setFalse: hideDeleting, + }] = useBoolean(false) + + const handleUpdate = useCallback(async (form: any) => { + const res = await updateMCP({ + ...form, + provider_id: data.id, + }) + if ((res as any)?.result === 'success') { + hideUpdateModal() + onUpdate() + } + }, [data, updateMCP, hideUpdateModal, onUpdate]) + + const handleDelete = useCallback(async () => { + showDeleting() + const res = await deleteMCP(data.id) + hideDeleting() + if ((res as any)?.result === 'success') { + hideDeleteConfirm() + onUpdate() + } + }, [data, showDeleting, hideDeleting, hideDeleteConfirm, onUpdate]) return (
handleSelect(data)} className={cn( - 'relative flex cursor-pointer flex-col rounded-xl border-[1.5px] border-transparent bg-components-card-bg shadow-xs hover:bg-components-card-bg-alt hover:shadow-md', + 'group relative flex cursor-pointer flex-col rounded-xl border-[1.5px] border-transparent bg-components-card-bg shadow-xs hover:bg-components-card-bg-alt hover:shadow-md', currentProvider?.id === data.id && 'border-components-option-card-option-selected-border bg-components-card-bg-alt', )} > -
+
@@ -68,6 +115,39 @@ const MCPCard = ({
)}
+ {isCurrentWorkspaceManager && ( + + )} + {isShowUpdateModal && ( + + )} + {isShowDeleteConfirm && ( + + {t('tools.mcp.deleteConfirmTitle', { mcp: data.name })} +
+ } + onCancel={hideDeleteConfirm} + onConfirm={handleDelete} + isLoading={deleting} + isDisabled={deleting} + /> + )} ) } diff --git a/web/i18n/en-US/tools.ts b/web/i18n/en-US/tools.ts index 1954f264f3..e4f965f041 100644 --- a/web/i18n/en-US/tools.ts +++ b/web/i18n/en-US/tools.ts @@ -169,8 +169,15 @@ const translation = { serverUrl: 'Server URL', serverUrlPlaceholder: 'URL to server endpiont', cancel: 'Cancel', + save: 'Save', confirm: 'Add & Authorize', }, + delete: 'Remove MCP Server', + deleteConfirmTitle: 'Would you like to remove {{mcp}}?', + operation: { + edit: 'Edit', + remove: 'Remove', + }, }, } diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index 1c5b5a81e1..990e445fa1 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -169,8 +169,15 @@ const translation = { serverUrl: '服务端点 URL', serverUrlPlaceholder: '服务端点的 URL', cancel: '取消', + save: '保存', confirm: '添加并授权', }, + delete: '删除 MCP 服务', + deleteConfirmTitle: '你想要删除 {{mcp}} 吗?', + operation: { + edit: '修改', + remove: '删除', + }, }, } diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts index 78797a2cb3..2d062819a6 100644 --- a/web/service/use-tools.ts +++ b/web/service/use-tools.ts @@ -1,4 +1,4 @@ -import { get, post } from './base' +import { del, get, post, put } from './base' import type { Collection, Tool, @@ -93,13 +93,54 @@ export const useCreateMCP = ({ icon: string icon_background?: string | null }) => { - console.log('payload', payload) - return Promise.resolve(payload) - // return post('/console/api/workspaces/current/tool-provider/mcp', { - // body: { - // ...payload, - // }, - // }) + return post('workspaces/current/tool-provider/mcp', { + body: { + ...payload, + }, + }) + }, + onSuccess, + }) +} + +export const useUpdateMCP = ({ + onSuccess, +}: { + onSuccess?: () => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'update-mcp'], + mutationFn: (payload: { + name: string + server_url: string + icon_type: AppIconType + icon: string + icon_background?: string | null + provider_id: string + }) => { + return put('workspaces/current/tool-provider/mcp', { + body: { + ...payload, + }, + }) + }, + onSuccess, + }) +} + +export const useDeleteMCP = ({ + onSuccess, +}: { + onSuccess?: () => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'delete-mcp'], + mutationFn: (id: string) => { + return del('/console/api/workspaces/current/tool-provider/mcp', { + body: { + provider_id: id, + }, + }) }, onSuccess, })