This is an automated email from the ASF dual-hosted git repository. achao pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/shenyu-dashboard.git
The following commit(s) were added to refs/heads/master by this push: new f0199666 [Feat] mcp server (#528) f0199666 is described below commit f0199666384fd7c92e15c8dbc8726bb32d171438 Author: aias00 <rok...@163.com> AuthorDate: Tue Jul 15 10:28:27 2025 +0800 [Feat] mcp server (#528) --- .github/workflows/build.yml | 4 +- .gitignore | 3 + src/common/router.js | 7 + src/locales/en-US.json | 25 + src/locales/zh-CN.json | 25 + src/models/mcpServer.js | 85 ++ src/routes/Plugin/Common/Selector.js | 2 +- src/routes/Plugin/McpServer/ToolsModal.js | 411 +++++++++ src/routes/Plugin/McpServer/index.js | 1385 +++++++++++++++++++++++++++++ src/services/api.js | 33 + 10 files changed, 1977 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 02afdb7d..131a7eae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x, 14.x, 16.x, 18.x] + node-version: [12.x, 14.x, 16.x, 18.x, 20.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} @@ -22,5 +22,5 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm install - run: npm run lint - if: matrix.node-version == '18.x' + if: matrix.node-version == '20.x' - run: npm run build --if-present diff --git a/.gitignore b/.gitignore index 9de6675e..24ba8043 100755 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ package-lock.json # visual studio .history dist/* + +# Private individual user cursor rules +.cursor/rules/_*.mdc diff --git a/src/common/router.js b/src/common/router.js index 3847aa11..b56037c2 100644 --- a/src/common/router.js +++ b/src/common/router.js @@ -95,6 +95,13 @@ export const getRouterData = (app) => { () => import("../layouts/BasicLayout"), ), }, + "/plug/Mcp/mcpServer": { + component: dynamicWrapper( + app, + ["mcpServer"], + () => import("../routes/Plugin/McpServer"), + ), + }, "/home": { component: dynamicWrapper(app, [], () => import("../routes/Home")), }, diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 4a436f44..70880969 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -24,11 +24,23 @@ "SHENYU.COMMON.SURE": "Sure", "SHENYU.COMMON.CALCEL": "Cancel", "SHENYU.COMMON.RULE.NAME": "RuleName", + "SHENYU.COMMON.TOOL.NAME": "ToolName", + "SHENYU.COMMON.RULE.HANDLE": "Handle", + "SHENYU.COMMON.TOOL.REQUESTMETHOD": "RequestMethod", + "SHENYU.COMMON.TOOL.REQUESTURI": "RequestURI", + "SHENYU.COMMON.TOOL.REQUESTPARAMS": "RequestParams", + "SHENYU.COMMON.TOOL.REQUESTCONFIG": "RequestConfig", "SHENYU.COMMON.SYN": "Synchronous", + "SHENYU.COMMON.PREVIEW": "Preview", "SHENYU.COMMON.ADD.RULE": "Add", + "SHENYU.COMMON.ADD.TOOL": "Add", "SHENYU.COMMON.ADD": "Add", "SHENYU.COMMON.COPY": "Copy", "SHENYU.COMMON.INPUTNAME": "InputName", + "SHENYU.COMMON.INPUTDESCRIPTION": "InputDescription", + "SHENYU.COMMON.INPUTREQUESTCONFIG": "InputRequestConfig", + "SHENYU.COMMON.INPUTREQUESTMETHOD": "InputRequestMethod", + "SHENYU.COMMON.INPUTREQUESTURI": "InputRequestURI", "SHENYU.COMMON.TYPE": "Type", "SHENYU.COMMON.INPUTTYPE": "InputType", "SHENYU.COMMON.SELECTOR.TYPE.FULL": "full", @@ -37,6 +49,10 @@ "SHENYU.COMMON.MATCHTYPE": "MatchType", "SHENYU.COMMON.INPUTMATCHTYPE": "MatchMode", "SHENYU.COMMON.CONDITION": "Conditions", + "SHENYU.COMMON.PARAMETER": "Parameter", + "SHENYU.COMMON.PARAMETER.NAME": "ParamName", + "SHENYU.COMMON.PARAMETER.TYPE": "ParamType", + "SHENYU.COMMON.PARAMETER.DESCRIPTION": "ParamDescription", "SHENYU.COMMON.DEAL": "Handler", "SHENYU.COMMON.DEAL.COMPONENT": "Component Handler", "SHENYU.COMMON.DEAL.CUSTOM": "Custom Handler", @@ -81,15 +97,23 @@ "SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACE": "Namespace", "SHENYU.MENU.CONFIG.MANAGMENT": "BasicConfig", "SHENYU.PLUGIN.SELECTOR.LIST.TITLE": "SelectorList", + "SHENYU.PLUGIN.SERVER.LIST.TITLE": "ServerList", "SHENYU.PLUGIN.SELECTOR.LIST.ADD": "Add", "SHENYU.PLUGIN.SELECTOR.BATCH.OPENED": "Batch Opened", "SHENYU.PLUGIN.SELECTOR.BATCH.CLOSED": "Batch Closed", "SHENYU.PLUGIN.SELECTOR.RULE.LIST": "RulesList", + "SHENYU.PLUGIN.SELECTOR.TOOL.LIST": "ToolList", "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME": "Name", + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.DESCRIPTION": "Description", + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTCONFIG": "RequestConfig", + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTMETHOD": "RequestMethod", + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTURI": "RequestURI", "SHENYU.PLUGIN.SEARCH.SELECTOR.NAME": "Name", "SHENYU.PLUGIN.SEARCH.RULE.NAME": "RuleName", + "SHENYU.PLUGIN.SEARCH.TOOL.NAME": "ToolName", "SHENYU.PLUGIN.SEARCH.RULE.COPY": "Copy Rule", "SHENYU.SELECTOR.NAME": "Selector", + "SHENYU.SERVER.NAME": "Server", "SHENYU.SELECTOR.COPY": "Copy Selector", "SHENYU.SELECTOR.CONTINUE": "Continued", "SHENYU.SELECTOR.PRINTLOG": "PrintLogs", @@ -100,6 +124,7 @@ "SHENYU.SELECTOR.INPUTORDER": "You can fill in the numeric flag execution order between 1 and 1000", "SHENYU.SELECTOR.SOURCE.PLACEHOLDER": "Please select the selector you want to copy", "SHENYU.RULE.NAME": "Rules", + "SHENYU.TOOL.NAME": "Tools", "SHENYU.RULE.SOURCE.PLACEHOLDER": "Please select the rule you want to copy", "SHENYU.COMMON.LOAD": "LoadPlugins", "SHENYU.COMMON.LOADSTRATEGY": "LoadStrategy", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index dc3e33f5..27fe04ac 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -23,11 +23,23 @@ "SHENYU.COMMON.SURE": "确认", "SHENYU.COMMON.CALCEL": "取消", "SHENYU.COMMON.RULE.NAME": "规则名称", + "SHENYU.COMMON.TOOL.NAME": "工具名称", + "SHENYU.COMMON.RULE.HANDLE": "处理", + "SHENYU.COMMON.TOOL.REQUESTMETHOD": "请求方法", + "SHENYU.COMMON.TOOL.REQUESTURI": "请求URI", + "SHENYU.COMMON.TOOL.REQUESTPARAMS": "请求参数", + "SHENYU.COMMON.TOOL.REQUESTCONFIG": "请求配置", "SHENYU.COMMON.SYN": "同步自定义", + "SHENYU.COMMON.PREVIEW": "预览", "SHENYU.COMMON.ADD.RULE": "添加规则", + "SHENYU.COMMON.ADD.TOOL": "添加工具", "SHENYU.COMMON.ADD": "新增", "SHENYU.COMMON.COPY": "复制", "SHENYU.COMMON.INPUTNAME": "请输入名称", + "SHENYU.COMMON.INPUTDESCRIPTION": "请输入描述", + "SHENYU.COMMON.INPUTREQUESTCONFIG": "请输入请求配置", + "SHENYU.COMMON.INPUTREQUESTMETHOD": "请输入请求方法", + "SHENYU.COMMON.INPUTREQUESTURI": "请输入请求URI", "SHENYU.COMMON.TYPE": "类型", "SHENYU.COMMON.INPUTTYPE": "请输入类型", "SHENYU.COMMON.SELECTOR.TYPE.FULL": "全流量", @@ -36,6 +48,10 @@ "SHENYU.COMMON.MATCHTYPE": "匹配方式", "SHENYU.COMMON.INPUTMATCHTYPE": "请选择匹配方式", "SHENYU.COMMON.CONDITION": "条件", + "SHENYU.COMMON.PARAMETER": "参数", + "SHENYU.COMMON.PARAMETER.NAME": "参数名称", + "SHENYU.COMMON.PARAMETER.TYPE": "参数类型", + "SHENYU.COMMON.PARAMETER.DESCRIPTION": "参数描述", "SHENYU.COMMON.DEAL": "处理", "SHENYU.COMMON.DEAL.COMPONENT": "组件处理", "SHENYU.COMMON.DEAL.CUSTOM": "自定义处理", @@ -82,15 +98,23 @@ "SHENYU.MENU.CONFIG.MANAGMENT": "基础配置", "SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACE": "命名空间管理", "SHENYU.PLUGIN.SELECTOR.LIST.TITLE": "选择器列表", + "SHENYU.PLUGIN.SERVER.LIST.TITLE": "服务列表", "SHENYU.PLUGIN.SELECTOR.LIST.ADD": "添加选择器", "SHENYU.PLUGIN.SELECTOR.BATCH.OPENED": "批量开启", "SHENYU.PLUGIN.SELECTOR.BATCH.CLOSED": "批量关闭", "SHENYU.PLUGIN.SELECTOR.RULE.LIST": "选择器规则列表", + "SHENYU.PLUGIN.SELECTOR.TOOL.LIST": "选择器工具列表", "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME": "名称", + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.DESCRIPTION": "描述", + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTCONFIG": "请求配置", + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTMETHOD": "请求方法", + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTURI": "请求URI", "SHENYU.PLUGIN.SEARCH.SELECTOR.NAME": "选择器名称", "SHENYU.PLUGIN.SEARCH.RULE.NAME": "规则名称", + "SHENYU.PLUGIN.SEARCH.TOOL.NAME": "工具名称", "SHENYU.PLUGIN.SEARCH.RULE.COPY": "复制规则", "SHENYU.SELECTOR.NAME": "选择器", + "SHENYU.SERVER.NAME": "服务", "SHENYU.SELECTOR.COPY": "复制选择器", "SHENYU.SELECTOR.CONTINUE": "继续后续选择器", "SHENYU.SELECTOR.PRINTLOG": "打印日志", @@ -101,6 +125,7 @@ "SHENYU.SELECTOR.INPUTORDER": "可以填写1-1000之间的数字标志执行先后顺序", "SHENYU.SELECTOR.SOURCE.PLACEHOLDER": "请选择需要复制的选择器", "SHENYU.RULE.NAME": "规则", + "SHENYU.TOOL.NAME": "工具", "SHENYU.RULE.SOURCE.PLACEHOLDER": "请选择需要复制的规则", "SHENYU.COMMON.LOAD": "负载", "SHENYU.COMMON.LOADSTRATEGY": "负载策略", diff --git a/src/models/mcpServer.js b/src/models/mcpServer.js new file mode 100644 index 00000000..2390bf1a --- /dev/null +++ b/src/models/mcpServer.js @@ -0,0 +1,85 @@ +import { message } from "antd"; +import { + fetchMcpServer, + addMcpServer, + updateMcpServer, + deleteMcpServer, +} from "../services/api"; +import { getIntlContent } from "../utils/IntlUtils"; + +export default { + namespace: "mcpServer", + + state: { + list: [], + total: 0, + currentPage: 1, + pageSize: 12, + }, + + effects: { + *fetch({ payload }, { call, put }) { + const response = yield call(fetchMcpServer, payload); + if (response) { + yield put({ + type: "saveList", + payload: { + list: response.data, + total: response.total, + currentPage: payload.currentPage, + pageSize: payload.pageSize, + }, + }); + } + }, + *add({ payload, callback }, { call, put }) { + const response = yield call(addMcpServer, payload); + if (response) { + message.success(getIntlContent("SHENYU.COMMON.RESPONSE.ADD.SUCCESS")); + yield put({ type: "reload" }); + } + if (callback) callback(); + }, + *update({ payload, callback }, { call, put }) { + const response = yield call(updateMcpServer, payload); + if (response) { + message.success( + getIntlContent("SHENYU.COMMON.RESPONSE.UPDATE.SUCCESS"), + ); + yield put({ type: "reload" }); + } + if (callback) callback(); + }, + *delete({ payload, callback }, { call, put }) { + const response = yield call(deleteMcpServer, payload); + if (response) { + message.success( + getIntlContent("SHENYU.COMMON.RESPONSE.DELETE.SUCCESS"), + ); + yield put({ type: "reload" }); + } + if (callback) callback(); + }, + *reload(_, { put, select }) { + const { currentPage, pageSize } = yield select( + (state) => state.mcpServer, + ); + yield put({ + type: "fetch", + payload: { currentPage, pageSize }, + }); + }, + }, + + reducers: { + saveList(state, { payload }) { + return { + ...state, + list: payload.list, + total: payload.total, + currentPage: payload.currentPage, + pageSize: payload.pageSize, + }; + }, + }, +}; diff --git a/src/routes/Plugin/Common/Selector.js b/src/routes/Plugin/Common/Selector.js index f2bf78a9..c9bbd5c9 100644 --- a/src/routes/Plugin/Common/Selector.js +++ b/src/routes/Plugin/Common/Selector.js @@ -1725,7 +1725,7 @@ class AddModal extends Component { <Modal width="1100px" centered - title={getIntlContent("SHENYU.SELECTOR.NAME")} + title={this.props.modalTitle || getIntlContent("SHENYU.SELECTOR.NAME")} // visible here defaults to true, because the visibility of modal is determined by the popup attribute in index.js visible okText={getIntlContent("SHENYU.COMMON.SURE")} diff --git a/src/routes/Plugin/McpServer/ToolsModal.js b/src/routes/Plugin/McpServer/ToolsModal.js new file mode 100644 index 00000000..73d87ddf --- /dev/null +++ b/src/routes/Plugin/McpServer/ToolsModal.js @@ -0,0 +1,411 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from "react"; +import { + Button, + Col, + Form, + Input, + message, + Modal, + Row, + Select, + Switch, +} from "antd"; +import { connect } from "dva"; +import TextArea from "antd/lib/input/TextArea"; +import ReactJson from "react-json-view"; +import styles from "../index.less"; +import { getIntlContent } from "../../../utils/IntlUtils"; +import RuleCopy from "../Common/RuleCopy"; + +const FormItem = Form.Item; +const { Option } = Select; + +@connect(({ global }) => ({ + currentNamespaceId: global.currentNamespaceId, +})) +class AddModal extends Component { + constructor(props) { + super(props); + this.state = { + visible: false, + questJson: {}, + }; + + this.initParameters(props); + } + + initParameters = (props) => { + let parameters = []; + let questJson = {}; + try { + const handle = props.handle ? JSON.parse(props.handle) : {}; + parameters = handle.parameters || [ + { + type: "String", + name: "", + description: "", + }, + ]; + questJson = JSON.parse(handle.requestConfig); + } catch (e) { + console.error("Failed to parse handle JSON:", e); + parameters = [ + { + type: "String", + name: "", + description: "", + }, + ]; + } + this.state.parameters = parameters; + this.state.questJson = questJson; + }; + + updateJson = (obj) => { + this.setState({ questJson: obj.updated_src }); + this.props.form.setFieldsValue({ + requestConfig: JSON.stringify(obj.updated_src), + }); + }; + + checkParams = () => { + let { parameters } = this.state; + let result = true; + if (parameters) { + parameters.forEach((item, index) => { + const { type, name, description } = item; + if (!type || !name || !description) { + message.destroy(); + message.error(`Line ${index + 1} param is incomplete`); + result = false; + } + // eslint-disable-next-line no-lonely-if + if (!name) { + message.destroy(); + message.error(`Line ${index + 1} param is incomplete`); + result = false; + } + }); + } + return result; + }; + + handleSubmit = (e) => { + e.preventDefault(); + const { form, handleOk } = this.props; + const { parameters } = this.state; + + form.validateFieldsAndScroll((err, values) => { + const { name, description, enabled } = values; + if (!err) { + const submit = this.checkParams(); + if (submit) { + let handle = { + parameters, + requestConfig: JSON.stringify(this.state.questJson), + description, + }; + handle = JSON.stringify({ + ...handle, + }); + handleOk({ + name, + description, + handle, + enabled, + sort: 1, + loged: true, + matchMode: "0", + matchRestful: false, + ruleConditions: [ + { + paramType: "uri", + operator: "pathPattern", + paramName: "/", + paramValue: "/**", + }, + ], + }); + } + } + }); + }; + + handleAdd = () => { + let { parameters } = this.state; + parameters.push({ + type: "String", + name: "", + description: "", + }); + + this.setState({ parameters }, () => { + let len = parameters.length || 0; + let key = `typeValueEn${len - 1}`; + this.setState({ [key]: true }); + }); + }; + + handleDelete = (index) => { + let { parameters } = this.state; + parameters.splice(index, 1); + this.setState({ parameters }); + }; + + handleCopyData = (copyData) => { + if (!copyData) { + this.setState({ visible: false }); + return; + } + const { form } = this.props; + const { name, matchMode, loged, enabled, sort } = copyData; + const formData = { + name, + matchMode: matchMode.toString(), + loged, + enabled, + sort, + }; + form.setFieldsValue(formData); + this.setState({ visible: false }); + }; + + render() { + let { + onCancel, + form, + name = "", + description = "", + enabled = true, + handle = "{}", + } = this.props; + const { parameters, visible, questJson } = this.state; + + // Parse handle JSON to get requestConfig and description + let parsedHandle = {}; + try { + parsedHandle = JSON.parse(handle); + } catch (e) { + console.error("Failed to parse handle JSON:", e); + } + + const { description: handleDescription = "" } = parsedHandle; + // Use description from handle if available, otherwise use from props + const finalDescription = handleDescription || description; + + const { getFieldDecorator } = form; + const formItemLayout = { + labelCol: { + sm: { span: 4 }, + }, + wrapperCol: { + sm: { span: 20 }, + }, + }; + return ( + <Modal + width={1000} + centered + title={getIntlContent("SHENYU.TOOL.NAME")} + visible + okText={getIntlContent("SHENYU.COMMON.SURE")} + cancelText={getIntlContent("SHENYU.COMMON.CALCEL")} + onOk={this.handleSubmit} + onCancel={onCancel} + > + <Form onSubmit={this.handleSubmit} className="login-form"> + <FormItem + label={getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME")} + {...formItemLayout} + > + {getFieldDecorator("name", { + rules: [ + { + required: true, + message: getIntlContent("SHENYU.COMMON.INPUTNAME"), + }, + ], + initialValue: name, + })( + <Input + allowClear + placeholder={getIntlContent( + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME", + )} + addonAfter={ + <Button + size="small" + type="link" + onClick={() => { + this.setState({ visible: true }); + }} + > + {getIntlContent("SHENYU.PLUGIN.SEARCH.RULE.COPY")} + </Button> + } + />, + )} + </FormItem> + <RuleCopy + visible={visible} + onOk={this.handleCopyData} + onCancel={() => { + this.setState({ visible: false }); + }} + /> + <FormItem + label={getIntlContent( + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.DESCRIPTION", + )} + {...formItemLayout} + > + {getFieldDecorator("description", { + rules: [ + { + required: true, + message: getIntlContent("SHENYU.COMMON.INPUTDESCRIPTION"), + }, + ], + initialValue: finalDescription, + })( + <TextArea + allowClear + placeholder={getIntlContent( + "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.DESCRIPTION", + )} + />, + )} + </FormItem> + <div className={styles.condition}> + <FormItem + label={getIntlContent("SHENYU.COMMON.PARAMETER")} + {...formItemLayout} + > + {parameters.map((item, index) => { + return ( + <Row key={index} gutter={8}> + <Col span={4}> + <Input + allowClear + value={item.name} + placeholder={getIntlContent( + "SHENYU.COMMON.PARAMETER.NAME", + )} + onChange={(e) => { + const newValue = e.target.value; + const newParameters = [...parameters]; + newParameters[index].name = newValue; + this.setState({ parameters: newParameters }); + }} + /> + </Col> + <Col span={5}> + <Select + value={item.type} + placeholder={getIntlContent( + "SHENYU.COMMON.PARAMETER.TYPE", + )} + onChange={(value) => { + const newParameters = [...parameters]; + newParameters[index].type = value; + this.setState({ parameters: newParameters }); + }} + > + <Option value="String">String</Option> + <Option value="Integer">Integer</Option> + <Option value="Long">Long</Option> + <Option value="Double">Double</Option> + <Option value="Float">Float</Option> + <Option value="Boolean">Boolean</Option> + </Select> + </Col> + <Col span={11}> + <Input + allowClear + value={item.description} + placeholder={getIntlContent( + "SHENYU.COMMON.PARAMETER.DESCRIPTION", + )} + onChange={(e) => { + const newValue = e.target.value; + const newParameters = [...parameters]; + newParameters[index].description = newValue; + this.setState({ parameters: newParameters }); + }} + /> + </Col> + <Col span={4}> + <Button + type="danger" + onClick={() => { + this.handleDelete(index); + }} + > + {getIntlContent("SHENYU.COMMON.DELETE.NAME")} + </Button> + </Col> + </Row> + ); + })} + </FormItem> + <FormItem label={" "} colon={false} {...formItemLayout}> + <Button + className={styles.addButton} + onClick={this.handleAdd} + type="primary" + > + {getIntlContent("SHENYU.COMMON.ADD")}{" "} + {getIntlContent("SHENYU.COMMON.PARAMETER")} + </Button> + </FormItem> + </div> + <FormItem + label={getIntlContent("SHENYU.COMMON.TOOL.REQUESTCONFIG")} + {...formItemLayout} + required={true} + > + <ReactJson + src={questJson} + theme="monokai" + displayDataTypes={false} + name={false} + onAdd={this.updateJson} + onEdit={this.updateJson} + onDelete={this.updateJson} + style={{ borderRadius: 4, padding: 16 }} + /> + </FormItem> + <FormItem + {...formItemLayout} + label={getIntlContent("SHENYU.SELECTOR.WHETHEROPEN")} + > + {getFieldDecorator("enabled", { + initialValue: enabled, + valuePropName: "checked", + rules: [{ required: true }], + })(<Switch />)} + </FormItem> + </Form> + </Modal> + ); + } +} + +export default Form.create()(AddModal); diff --git a/src/routes/Plugin/McpServer/index.js b/src/routes/Plugin/McpServer/index.js new file mode 100755 index 00000000..ea497ed7 --- /dev/null +++ b/src/routes/Plugin/McpServer/index.js @@ -0,0 +1,1385 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from "react"; +import { + Button, + Col, + Input, + message, + Popconfirm, + Popover, + Row, + Switch, + Table, + Tag, + Typography, +} from "antd"; +import { connect } from "dva"; +import ReactJson from "react-json-view"; +import styles from "../index.less"; +import Selector from "../Common/Selector"; +import Tools from "./ToolsModal"; +import { getCurrentLocale, getIntlContent } from "../../../utils/IntlUtils"; +import AuthButton from "../../../utils/AuthButton"; +import { + getUpdateModal, + updateNamespacePluginsEnabledByNamespace, +} from "../../../utils/namespacePlugin"; + +const { Search } = Input; +const { Title } = Typography; +@connect(({ common, global, loading }) => ({ + ...global, + ...common, + loading: loading.effects["global/fetchPlatform"], +})) +export default class McpServer extends Component { + constructor(props) { + super(props); + this.state = { + selectorPage: 1, + selectorPageSize: 12, + selectorSelectedRowKeys: [], + toolPage: 1, + toolPageSize: 12, + toolSelectedRowKeys: [], + popup: "", + localeName: "", + selectorName: undefined, + toolName: undefined, + isPluginEnabled: false, + pluginName: "mcpServer", + pluginRole: "Mcp", + }; + } + + componentDidMount() { + const { dispatch, plugins } = this.props; + const { selectorPage, selectorPageSize } = this.state; + if (plugins && plugins.length > 0) { + this.getAllSelectors(selectorPage, selectorPageSize, plugins); + } else { + dispatch({ + type: "global/fetchPlugins", + payload: { + callback: (pluginList) => { + this.getAllSelectors(selectorPage, selectorPageSize, pluginList); + }, + }, + }); + } + } + + /* eslint-disable no-unused-vars */ + componentDidUpdate(prevProps, prevState, snapshot) { + const preId = prevProps.match.params.id; + const newId = this.props.match.params.id; + const { selectorPage, selectorPageSize } = this.state; + const { dispatch, plugins, currentNamespaceId } = this.props; + if (newId !== preId) { + dispatch({ + type: "common/resetData", + }); + + if (prevProps.plugins && prevProps.plugins.length > 0) { + this.getAllSelectors(selectorPage, selectorPageSize, prevProps.plugins); + } else { + dispatch({ + type: "global/fetchPlugins", + payload: { + callback: (pluginList) => { + this.getAllSelectors(selectorPage, selectorPageSize, pluginList); + }, + }, + }); + } + } + if (prevProps.currentNamespaceId !== currentNamespaceId) { + if (plugins) { + this.getAllSelectors(selectorPage, selectorPageSize, plugins); + } + } + } + /* eslint-enable no-unused-vars */ + + componentWillUnmount() { + const { dispatch } = this.props; + dispatch({ + type: "common/resetData", + }); + } + + getAllSelectors = (page, pageSize, plugins) => { + const { dispatch, currentNamespaceId } = this.props; + const { selectorName } = this.state; + const tempPlugin = this.getPlugin(plugins, this.state.pluginName); + const tempPluginId = tempPlugin?.pluginId; + const enabled = tempPlugin?.enabled ?? false; + this.setState({ pluginId: tempPluginId, isPluginEnabled: enabled }); + dispatch({ + type: "common/fetchSelector", + payload: { + currentPage: page, + pageSize, + pluginId: tempPluginId, + name: selectorName, + namespaceId: currentNamespaceId, + }, + }); + this.setState({ selectorSelectedRowKeys: [] }); + this.setState({ toolSelectedRowKeys: [] }); + }; + + getAllTools = (page, pageSize) => { + const { dispatch, currentSelector, currentNamespaceId } = this.props; + const { toolName } = this.state; + const selectorId = currentSelector ? currentSelector.id : ""; + dispatch({ + type: "common/fetchRule", + payload: { + selectorId, + currentPage: page, + pageSize, + name: toolName, + namespaceId: currentNamespaceId, + }, + }); + this.setState({ selectorSelectedRowKeys: [] }); + this.setState({ toolSelectedRowKeys: [] }); + }; + + getPlugin = (plugins, name) => { + const plugin = plugins.filter((item) => { + return item.name === name; + }); + return plugin && plugin.length > 0 ? plugin[0] : null; + }; + + getPluginConfigField = (config, fieldName) => { + if (config) { + let configObj = JSON.parse(config); + return configObj[fieldName]; + } else { + return ""; + } + }; + + closeModal = () => { + this.setState({ popup: "" }); + }; + + searchSelectorOnchange = (e) => { + const selectorName = e.target.value; + this.setState({ selectorName }); + }; + + searchSelector = () => { + const { plugins } = this.props; + const { selectorPage, selectorPageSize } = this.state; + this.getAllSelectors(selectorPage, selectorPageSize, plugins); + }; + + isDiscovery = (pluginId) => { + // 5: divide + // 15: grpc + // 26: websocket + return ["5", "15", "26"].includes(pluginId); + }; + + addSelector = () => { + const { selectorPage, selectorPageSize, pluginName } = this.state; + const { dispatch, plugins, currentNamespaceId } = this.props; + const plugin = this.getPlugin(plugins, pluginName); + const { pluginId, config } = plugin; + const multiSelectorHandle = + this.getPluginConfigField(config, "multiSelectorHandle") === "1"; + const isDiscovery = this.isDiscovery(pluginId); + if (isDiscovery) { + let discoveryConfig = { + discoveryType: "", + serverList: "", + handler: {}, + listenerNode: "", + props: {}, + }; + this.setState({ + popup: ( + <Selector + modalTitle={getIntlContent("SHENYU.SERVER.NAME")} + pluginName + pluginId={pluginId} + multiSelectorHandle={multiSelectorHandle} + isAdd={true} + selectorConditions={[ + { + paramType: "uri", + operator: "match", + paramName: "/**", + paramValue: "", + }, + ]} + discoveryConfig={discoveryConfig} + isDiscovery={true} + handleOk={(selector) => { + const { + name: selectorName, + listenerNode, + serverList, + selectedDiscoveryType, + discoveryProps, + handler, + upstreams, + importedDiscoveryId, + } = selector; + const upstreamsWithProps = this.getUpstreamsWithProps(upstreams); + dispatch({ + type: "common/addSelector", + payload: { + pluginId, + ...selector, + upstreams: upstreamsWithProps, + namespaceId: currentNamespaceId, + }, + fetchValue: { + pluginId, + currentPage: selectorPage, + pageSize: selectorPageSize, + namespaceId: currentNamespaceId, + }, + callback: (selectorId) => { + this.addDiscoveryUpstream({ + selectorId, + selectorName, + pluginName, + listenerNode, + handler, + typeValue: this.getTypeValueByPluginName(pluginName), + upstreamsWithProps, + importedDiscoveryId, + selectedDiscoveryType, + serverList, + discoveryProps, + namespaceId: currentNamespaceId, + }); + this.closeModal(); + }, + }); + }} + onCancel={this.closeModal} + /> + ), + }); + } else { + this.setState({ + popup: ( + <Selector + pluginName={pluginName} + pluginId={pluginId} + multiSelectorHandle={multiSelectorHandle} + isDiscovery={false} + handleOk={(selector) => { + dispatch({ + type: "common/addSelector", + payload: { + pluginId, + ...selector, + namespaceId: currentNamespaceId, + }, + fetchValue: { + pluginId, + currentPage: selectorPage, + pageSize: selectorPageSize, + namespaceId: currentNamespaceId, + }, + callback: () => { + this.closeModal(); + }, + }); + }} + onCancel={this.closeModal} + /> + ), + }); + } + }; + + searchToolOnchange = (e) => { + const toolName = e.target.value; + this.setState({ toolName }); + }; + + searchTool = () => { + this.setState({ toolPage: 1 }); + const { toolPageSize } = this.state; + this.getAllTools(1, toolPageSize); + }; + + addTool = () => { + const { toolPage, toolPageSize, pluginId, pluginName } = this.state; + const { dispatch, currentSelector, plugins, currentNamespaceId } = + this.props; + const plugin = this.getPlugin(plugins, this.state.pluginName); + const { config } = plugin; + const multiRuleHandle = + this.getPluginConfigField(config, "multiRuleHandle") === "1"; + if (currentSelector && currentSelector.id) { + const selectorId = currentSelector.id; + this.setState({ + popup: ( + <Tools + pluginId={pluginId} + pluginName={pluginName} + multiRuleHandle={multiRuleHandle} + handleOk={(rule) => { + dispatch({ + type: "common/addRule", + payload: { + selectorId, + ...rule, + namespaceId: currentNamespaceId, + }, + fetchValue: { + selectorId, + currentPage: toolPage, + pageSize: toolPageSize, + namespaceId: currentNamespaceId, + }, + callback: () => { + this.closeModal(); + }, + }); + }} + onCancel={this.closeModal} + /> + ), + }); + } else { + message.destroy(); + message.warn(getIntlContent("SHENYU.COMMON.WARN.INPUT_SELECTOR")); + } + }; + + togglePluginStatus = () => { + const { dispatch, plugins } = this.props; + const pluginName = this.state.pluginName; + const plugin = this.getPlugin(plugins, pluginName); + const enabled = !this.state.isPluginEnabled; + updateNamespacePluginsEnabledByNamespace({ + list: [plugin.pluginId], + namespaceId: this.props.currentNamespaceId, + enabled, + dispatch, + callback: () => { + plugin.enabled = enabled; + this.setState({ isPluginEnabled: enabled }); + this.closeModal(); + }, + }); + }; + + editClick = () => { + const { dispatch, plugins } = this.props; + const plugin = this.getPlugin(plugins, this.state.pluginName); + getUpdateModal({ + id: plugin.id, + namespaceId: plugin.namespaceId, + dispatch, + callback: (popup) => { + this.setState({ popup }); + }, + updatedCallback: ({ enabled }) => { + this.setState({ isPluginEnabled: enabled }); + this.closeModal(); + }, + canceledCallback: () => { + this.closeModal(); + }, + }); + }; + + getTypeValueByPluginName = (name) => { + return name === "divide" + ? "http" + : name === "websocket" + ? "ws" + : name === "grpc" + ? "grpc" + : "http"; + }; + + getUpstreamsWithProps = (upstreams) => { + const { currentNamespaceId } = this.props; + return upstreams.map((item) => ({ + protocol: item.protocol, + url: item.url, + status: parseInt(item.status, 10), + weight: item.weight, + startupTime: item.startupTime, + props: JSON.stringify({ + warmupTime: item.warmupTime, + gray: `${item.gray}`, + }), + namespaceId: currentNamespaceId, + })); + }; + + addDiscoveryUpstream = ({ + selectorId, + selectorName, + pluginName, + listenerNode, + handler, + typeValue, + upstreamsWithProps, + importedDiscoveryId, + selectedDiscoveryType, + serverList, + discoveryProps, + namespaceId, + }) => { + const { dispatch } = this.props; + dispatch({ + type: "discovery/bindSelector", + payload: { + selectorId, + name: selectorName, + pluginName, + listenerNode, + handler, + type: typeValue, + discoveryUpstreams: upstreamsWithProps, + discovery: { + id: importedDiscoveryId, + discoveryType: selectedDiscoveryType, + serverList, + props: discoveryProps, + name: selectorName, + }, + namespaceId, + }, + }); + }; + + updateDiscoveryUpstream = (discoveryHandlerId, upstreams) => { + const { dispatch, currentNamespaceId } = this.props; + const upstreamsWithHandlerId = upstreams.map((item) => ({ + protocol: item.protocol, + url: item.url, + status: parseInt(item.status, 10), + weight: item.weight, + props: JSON.stringify({ + warmupTime: item.warmupTime, + gray: `${item.gray}`, + }), + discoveryHandlerId, + namespaceId: currentNamespaceId, + })); + dispatch({ + type: "discovery/updateDiscoveryUpstream", + payload: { + discoveryHandlerId, + upstreams: upstreamsWithHandlerId, + }, + }); + }; + + editSelector = (record) => { + const { dispatch, plugins, currentNamespaceId } = this.props; + const { selectorPage, selectorPageSize, pluginName } = this.state; + const plugin = this.getPlugin(plugins, pluginName); + const { pluginId, config } = plugin; + const multiSelectorHandle = + this.getPluginConfigField(config, "multiSelectorHandle") === "1"; + const isDiscovery = this.isDiscovery(pluginId); + const { id } = record; + dispatch({ + type: "common/fetchSeItem", + payload: { + id, + namespaceId: currentNamespaceId, + }, + callback: (selector) => { + if (isDiscovery) { + let discoveryConfig = { + props: + selector.discoveryVO && selector.discoveryVO.props + ? selector.discoveryVO.props + : "{}", + discoveryType: + selector.discoveryVO && selector.discoveryVO.type + ? selector.discoveryVO.type + : "local", + serverList: + selector.discoveryVO && selector.discoveryVO.serverList + ? selector.discoveryVO.serverList + : "", + handler: + selector.discoveryHandler && selector.discoveryHandler.handler + ? selector.discoveryHandler.handler + : "{}", + listenerNode: + selector.discoveryHandler && + selector.discoveryHandler.listenerNode + ? selector.discoveryHandler.listenerNode + : "", + }; + let updateArray = []; + if (selector.discoveryUpstreams) { + updateArray = selector.discoveryUpstreams.map((item) => { + let propsObj = JSON.parse(item.props || "{}"); + if (item.props === null) { + propsObj = { + warmupTime: 10, + gray: "false", + }; + } + return { + ...item, + key: item.id, + warmupTime: propsObj.warmupTime, + gray: propsObj.gray, + }; + }); + } + let discoveryHandlerId = selector.discoveryHandler + ? selector.discoveryHandler.id + : ""; + this.setState({ + popup: ( + <Selector + pluginName={pluginName} + {...selector} + multiSelectorHandle={multiSelectorHandle} + discoveryConfig={discoveryConfig} + discoveryUpstreams={updateArray} + isAdd={false} + isDiscovery={true} + handleOk={(values) => { + dispatch({ + type: "common/updateSelector", + payload: { + pluginId, + ...values, + id, + namespaceId: currentNamespaceId, + }, + fetchValue: { + pluginId, + currentPage: selectorPage, + pageSize: selectorPageSize, + namespaceId: currentNamespaceId, + }, + callback: () => { + const { + name: selectorName, + handler, + upstreams, + serverList, + listenerNode, + discoveryProps, + importedDiscoveryId, + selectedDiscoveryType, + } = values; + + if (!discoveryHandlerId) { + this.addDiscoveryUpstream({ + selectorId: id, + selectorName, + pluginName, + listenerNode, + handler, + typeValue: this.getTypeValueByPluginName(pluginName), + upstreamsWithProps: + this.getUpstreamsWithProps(upstreams), + importedDiscoveryId, + selectedDiscoveryType, + serverList, + discoveryProps, + namespaceId: currentNamespaceId, + }); + } else { + this.updateDiscoveryUpstream( + discoveryHandlerId, + upstreams, + ); + } + this.closeModal(); + }, + }); + }} + onCancel={this.closeModal} + /> + ), + }); + } else { + this.setState({ + popup: ( + <Selector + pluginName={pluginName} + pluginId={pluginId} + {...selector} + multiSelectorHandle={multiSelectorHandle} + isDiscovery={false} + handleOk={(values) => { + dispatch({ + type: "common/updateSelector", + payload: { + pluginId, + ...values, + id, + namespaceId: currentNamespaceId, + }, + fetchValue: { + pluginId, + currentPage: selectorPage, + pageSize: selectorPageSize, + namespaceId: currentNamespaceId, + }, + callback: () => { + this.closeModal(); + }, + }); + }} + onCancel={this.closeModal} + /> + ), + }); + } + }, + }); + }; + + enableSelector = ({ list, enabled }) => { + const { dispatch, plugins, currentNamespaceId } = this.props; + const { selectorPage, selectorPageSize, pluginName } = this.state; + const plugin = this.getPlugin(plugins, pluginName); + const { pluginId } = plugin; + dispatch({ + type: "common/enableSelector", + payload: { + list, + enabled, + namespaceId: currentNamespaceId, + }, + fetchValue: { + pluginId, + currentPage: selectorPage, + pageSize: selectorPageSize, + namespaceId: currentNamespaceId, + }, + }); + }; + + onSelectorSelectChange = (selectorSelectedRowKeys) => { + this.setState({ selectorSelectedRowKeys }); + }; + + openSelectorClick = () => { + const { selectorSelectedRowKeys } = this.state; + const { selectorList } = this.props; + if (selectorSelectedRowKeys && selectorSelectedRowKeys.length > 0) { + let anyEnabled = selectorList.some( + (selector) => + selectorSelectedRowKeys.includes(selector.id) && selector.enabled, + ); + this.enableSelector({ + list: selectorSelectedRowKeys, + enabled: !anyEnabled, + }); + } else { + message.destroy(); + message.warn("Please select data"); + } + }; + + deleteSelector = (record) => { + const { dispatch, plugins, currentNamespaceId } = this.props; + const { selectorPage, selectorPageSize, pluginName } = this.state; + const pluginId = this.getPluginId(plugins, pluginName); + dispatch({ + type: "common/deleteSelector", + payload: { + list: [record.id], + namespaceId: currentNamespaceId, + }, + fetchValue: { + pluginId, + currentPage: selectorPage, + pageSize: selectorPageSize, + namespaceId: currentNamespaceId, + }, + }); + }; + + pageSelectorChange = (page) => { + this.setState({ selectorPage: page }); + const { plugins } = this.props; + const { selectorPageSize } = this.state; + this.getAllSelectors(page, selectorPageSize, plugins); + }; + + pageSelectorChangeSize = (currentPage, pageSize) => { + const { plugins } = this.props; + this.setState({ selectorPage: 1, selectorPageSize: pageSize }); + this.getAllSelectors(1, pageSize, plugins); + }; + + // select + rowClick = (record) => { + const { id } = record; + const { dispatch, currentNamespaceId } = this.props; + const { selectorPageSize } = this.state; + dispatch({ + type: "common/saveCurrentSelector", + payload: { + currentSelector: record, + }, + }); + dispatch({ + type: "common/fetchRule", + payload: { + currentPage: 1, + pageSize: selectorPageSize, + selectorId: id, + namespaceId: currentNamespaceId, + }, + }); + }; + + pageToolChange = (page) => { + this.setState({ toolPage: page }); + const { toolPageSize } = this.state; + this.getAllTools(page, toolPageSize); + }; + + pageToolChangeSize = (currentPage, pageSize) => { + this.setState({ toolPage: 1, toolPageSize: pageSize }); + this.getAllTools(1, pageSize); + }; + + editTool = (record) => { + console.log("record", record); + const { dispatch, currentSelector, plugins, currentNamespaceId } = + this.props; + const { toolPage, toolPageSize, pluginId, pluginName } = this.state; + const plugin = this.getPlugin(plugins, this.state.pluginName); + const { config } = plugin; + const multiRuleHandle = + this.getPluginConfigField(config, "multiRuleHandle") === "1"; + const selectorId = currentSelector ? currentSelector.id : ""; + const { id } = record; + dispatch({ + type: "common/fetchRuleItem", + payload: { + id, + namespaceId: currentNamespaceId, + }, + callback: (rule) => { + this.setState({ + popup: ( + <Tools + {...rule} + pluginId={pluginId} + pluginName={pluginName} + multiRuleHandle={multiRuleHandle} + handleOk={(values) => { + dispatch({ + type: "common/updateRule", + payload: { + selectorId, + ...values, + id, + namespaceId: currentNamespaceId, + }, + fetchValue: { + selectorId, + currentPage: toolPage, + pageSize: toolPageSize, + namespaceId: currentNamespaceId, + }, + callback: () => { + this.closeModal(); + }, + }); + }} + onCancel={this.closeModal} + /> + ), + }); + }, + }); + }; + + enableTool = ({ list, enabled }) => { + const { toolPage, toolPageSize } = this.state; + const { dispatch, currentSelector, currentNamespaceId } = this.props; + const selectorId = currentSelector ? currentSelector.id : ""; + dispatch({ + type: "common/enableRule", + payload: { + list, + enabled, + namespaceId: currentNamespaceId, + }, + fetchValue: { + selectorId, + currentPage: toolPage, + pageSize: toolPageSize, + namespaceId: currentNamespaceId, + }, + }); + }; + + onToolSelectChange = (toolSelectedRowKeys) => { + if (toolSelectedRowKeys && toolSelectedRowKeys.length > 0) { + this.setState({ toolSelectedRowKeys }); + } else { + this.setState({ toolSelectedRowKeys: [] }); + } + }; + + openToolClick = () => { + const { toolSelectedRowKeys } = this.state; + const { ruleList } = this.props; + if (toolSelectedRowKeys && toolSelectedRowKeys.length > 0) { + let anyEnabled = ruleList + ? ruleList.some( + (tool) => toolSelectedRowKeys.includes(tool.id) && tool.enabled, + ) + : false; + this.enableTool({ + list: toolSelectedRowKeys, + enabled: !anyEnabled, + }); + } else { + message.destroy(); + message.warn("Please select data"); + } + }; + + deleteTool = (record) => { + const { dispatch, currentSelector, ruleList, currentNamespaceId } = + this.props; + const { toolPage, toolPageSize } = this.state; + const currentPage = + toolPage > 1 && ruleList.length === 1 ? toolPage - 1 : toolPage; + dispatch({ + type: "common/deleteRule", + payload: { + list: [record.id], + namespaceId: currentNamespaceId, + }, + fetchValue: { + selectorId: currentSelector.id, + currentPage, + pageSize: toolPageSize, + namespaceId: currentNamespaceId, + }, + callback: () => { + this.setState({ toolSelectedRowKeys: [] }); + }, + }); + }; + + asyncClick = () => { + const { dispatch, plugins } = this.props; + const plugin = this.getPlugin(plugins, this.state.pluginName); + dispatch({ + type: "global/asyncPlugin", + payload: { + id: plugin.id, + }, + }); + }; + + // eslint-disable-next-line react/no-unused-class-component-methods + changeLocales(locale) { + this.setState({ + localeName: locale, + }); + getCurrentLocale(this.state.localeName); + } + + render() { + const { + popup, + selectorPage, + selectorPageSize, + selectorSelectedRowKeys, + toolPage, + toolPageSize, + toolSelectedRowKeys, + } = this.state; + const { + ruleList, + selectorList, + selectorTotal, + currentSelector, + toolTotal, + } = this.props; + + const selectColumns = [ + { + align: "center", + title: getIntlContent("SHENYU.SELECTOR.EXEORDER"), + dataIndex: "sort", + key: "sort", + }, + { + align: "center", + title: getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME"), + dataIndex: "name", + key: "name", + }, + { + align: "center", + title: getIntlContent("SHENYU.COMMON.OPEN"), + dataIndex: "enabled", + key: "enabled", + render: (text, row) => ( + <Switch + checkedChildren={getIntlContent("SHENYU.COMMON.OPEN")} + unCheckedChildren={getIntlContent("SHENYU.COMMON.CLOSE")} + checked={text} + onChange={(checked) => { + this.enableSelector({ list: [row.id], enabled: checked }); + }} + /> + ), + }, + { + align: "center", + title: getIntlContent("SHENYU.COMMON.OPERAT"), + dataIndex: "operate", + key: "operate", + render: (text, record) => { + return ( + <div> + <AuthButton + perms={`plugin:${this.state.pluginName}Selector:edit`} + > + <span + style={{ marginRight: 8 }} + className="edit" + onClick={(e) => { + e.stopPropagation(); + this.editSelector(record); + }} + > + {getIntlContent("SHENYU.COMMON.CHANGE")} + </span> + </AuthButton> + <AuthButton + perms={`plugin:${this.state.pluginName}Selector:delete`} + > + <Popconfirm + title={getIntlContent("SHENYU.COMMON.DELETE")} + placement="bottom" + onCancel={(e) => { + e.stopPropagation(); + }} + onConfirm={(e) => { + e.stopPropagation(); + this.deleteSelector(record); + }} + okText={getIntlContent("SHENYU.COMMON.SURE")} + cancelText={getIntlContent("SHENYU.COMMON.CALCEL")} + > + <span + className="edit" + onClick={(e) => { + e.stopPropagation(); + }} + > + {getIntlContent("SHENYU.COMMON.DELETE.NAME")} + </span> + </Popconfirm> + </AuthButton> + </div> + ); + }, + }, + ]; + const selectorRowSelection = { + selectedRowKeys: selectorSelectedRowKeys, + onChange: this.onSelectorSelectChange, + }; + + const toolRowSelection = { + selectedRowKeys: toolSelectedRowKeys, + onChange: this.onToolSelectChange, + }; + + const toolsColumns = [ + { + align: "center", + title: getIntlContent("SHENYU.SELECTOR.EXEORDER"), + dataIndex: "sort", + key: "sort", + }, + { + align: "center", + title: getIntlContent("SHENYU.COMMON.TOOL.NAME"), + dataIndex: "name", + key: "name", + }, + { + align: "center", + title: getIntlContent("SHENYU.COMMON.TOOL.REQUESTPARAMS"), + dataIndex: "handle", + key: "requestParams", + render: (text) => { + const handle = JSON.parse(text); + const parameters = handle.parameters; + return ( + <Popover + content={ + <ReactJson + name={false} + displayDataTypes={false} + src={parameters} + theme="monokai" + /> + } + title={getIntlContent("SHENYU.COMMON.TOOL.REQUESTPARAMS")} + > + <a>{getIntlContent("SHENYU.COMMON.PREVIEW")}</a> + </Popover> + ); + }, + }, + { + align: "center", + title: getIntlContent("SHENYU.COMMON.TOOL.REQUESTCONFIG"), + dataIndex: "handle", + key: "requestConfig", + render: (text) => { + const handle = JSON.parse(text); + const requestConfig = JSON.parse(handle.requestConfig); + return ( + <Popover + content={ + <ReactJson + name={false} + displayDataTypes={false} + src={requestConfig} + theme="monokai" + /> + } + title={getIntlContent("SHENYU.COMMON.TOOL.REQUESTCONFIG")} + > + <a>{getIntlContent("SHENYU.COMMON.PREVIEW")}</a> + </Popover> + ); + }, + }, + { + align: "center", + title: getIntlContent("SHENYU.COMMON.OPEN"), + dataIndex: "enabled", + key: "enabled", + render: (text, row) => ( + <Switch + checkedChildren={getIntlContent("SHENYU.COMMON.OPEN")} + unCheckedChildren={getIntlContent("SHENYU.COMMON.CLOSE")} + checked={text} + onChange={(checked) => { + this.enableTool({ list: [row.id], enabled: checked }); + }} + /> + ), + }, + { + align: "center", + title: getIntlContent("SHENYU.SYSTEM.UPDATETIME"), + dataIndex: "dateCreated", + key: "dateCreated", + sorter: (a, b) => (a.dateCreated > b.dateCreated ? 1 : -1), + }, + { + align: "center", + title: getIntlContent("SHENYU.COMMON.OPERAT"), + dataIndex: "operate", + key: "operate", + render: (text, record) => { + return ( + <div> + <AuthButton perms={`plugin:${this.state.pluginName}Rule:edit`}> + <span + className="edit" + style={{ marginRight: 8 }} + onClick={(e) => { + e.stopPropagation(); + this.editTool(record); + }} + > + {getIntlContent("SHENYU.COMMON.CHANGE")} + </span> + </AuthButton> + <AuthButton perms={`plugin:${this.state.pluginName}Rule:delete`}> + <Popconfirm + title={getIntlContent("SHENYU.COMMON.DELETE")} + placement="bottom" + onCancel={(e) => { + e.stopPropagation(); + }} + onConfirm={(e) => { + e.stopPropagation(); + this.deleteTool(record); + }} + okText={getIntlContent("SHENYU.COMMON.SURE")} + cancelText={getIntlContent("SHENYU.COMMON.CALCEL")} + > + <span + className="edit" + onClick={(e) => { + e.stopPropagation(); + }} + > + {getIntlContent("SHENYU.COMMON.DELETE.NAME")} + </span> + </Popconfirm> + </AuthButton> + </div> + ); + }, + }, + ]; + + const tag = { + text: this.state.isPluginEnabled + ? getIntlContent("SHENYU.COMMON.OPEN") + : getIntlContent("SHENYU.COMMON.CLOSE"), + color: this.state.isPluginEnabled ? "green" : "red", + }; + + const expandedRowRender = (record) => ( + <p + style={{ + maxWidth: document.documentElement.clientWidth * 0.5 - 50, + }} + > + {record.handle} + </p> + ); + + return ( + <div className="plug-content-wrap"> + <Row + style={{ + marginBottom: "5px", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }} + > + <div + style={{ display: "flex", alignItems: "end", flex: 1, margin: 0 }} + > + <Title + level={2} + style={{ textTransform: "capitalize", margin: "0 20px 0 0" }} + > + {this.state.pluginName} + </Title> + <Title level={3} type="secondary" style={{ margin: "0 20px 0 0" }}> + {this.state.pluginRole} + </Title> + <Tag color={tag.color}>{tag.text}</Tag> + </div> + <div + style={{ + display: "flex", + alignItems: "end", + gap: 10, + minHeight: 32, + }} + > + <Switch + checked={this.state.isPluginEnabled ?? false} + onChange={this.togglePluginStatus} + /> + <AuthButton perms="system:plugin:edit"> + <div className="edit" onClick={this.editClick}> + {getIntlContent("SHENYU.SYSTEM.EDITOR")} + </div> + </AuthButton> + </div> + </Row> + <Row gutter={20}> + <Col span={10}> + <div className="table-header"> + <h3>{getIntlContent("SHENYU.PLUGIN.SERVER.LIST.TITLE")}</h3> + <div className={styles.headerSearch}> + <AuthButton + perms={`plugin:${this.state.pluginName}Selector:query`} + > + <Search + className={styles.search} + style={{ minWidth: "130px" }} + placeholder={getIntlContent( + "SHENYU.PLUGIN.SEARCH.SELECTOR.NAME", + )} + enterButton={getIntlContent("SHENYU.SYSTEM.SEARCH")} + size="default" + onChange={this.searchSelectorOnchange} + onSearch={this.searchSelector} + /> + </AuthButton> + <AuthButton + perms={`plugin:${this.state.pluginName}Selector:add`} + > + <Button type="primary" onClick={this.addSelector}> + {getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.ADD")} + </Button> + </AuthButton> + <AuthButton + perms={`plugin:${this.state.pluginName}Selector:edit`} + > + <Button + type="primary" + onClick={this.openSelectorClick} + style={{ marginLeft: 10 }} + > + {getIntlContent( + selectorList.some( + (selector) => + selectorSelectedRowKeys.includes(selector.id) && + selector.enabled, + ) + ? "SHENYU.PLUGIN.SELECTOR.BATCH.CLOSED" + : "SHENYU.PLUGIN.SELECTOR.BATCH.OPENED", + )} + </Button> + </AuthButton> + </div> + </div> + <Table + size="small" + onRow={(record) => { + return { + onClick: () => { + this.rowClick(record); + }, + }; + }} + style={{ marginTop: 30 }} + bordered + columns={selectColumns} + dataSource={selectorList} + rowSelection={selectorRowSelection} + pagination={{ + total: selectorTotal, + showTotal: (showTotal) => `${showTotal}`, + showSizeChanger: true, + pageSizeOptions: ["12", "20", "50", "100"], + current: selectorPage, + pageSize: selectorPageSize, + onChange: this.pageSelectorChange, + onShowSizeChange: this.pageSelectorChangeSize, + }} + rowClassName={(item) => { + if (currentSelector && currentSelector.id === item.id) { + return "table-selected"; + } else { + return ""; + } + }} + /> + </Col> + <Col span={14}> + <div className="table-header"> + <div style={{ display: "flex", alignItems: "center" }}> + <h3>{getIntlContent("SHENYU.PLUGIN.SELECTOR.TOOL.LIST")}</h3> + <AuthButton perms={`plugin:${this.state.pluginName}:modify`}> + <Button + icon="reload" + onClick={this.asyncClick} + type="primary" + > + {getIntlContent("SHENYU.COMMON.SYN")}{" "} + {this.state.pluginName} + </Button> + </AuthButton> + </div> + + <div className={styles.headerSearch}> + <AuthButton perms={`plugin:${this.state.pluginName}Rule:query`}> + <Search + className={styles.search} + placeholder={getIntlContent( + "SHENYU.PLUGIN.SEARCH.TOOL.NAME", + )} + enterButton={getIntlContent("SHENYU.SYSTEM.SEARCH")} + size="default" + onChange={this.searchToolOnchange} + onSearch={this.searchTool} + /> + </AuthButton> + <AuthButton perms={`plugin:${this.state.pluginName}Rule:add`}> + <Button type="primary" onClick={this.addTool}> + {getIntlContent("SHENYU.COMMON.ADD.TOOL")} + </Button> + </AuthButton> + <AuthButton perms={`plugin:${this.state.pluginName}Rule:edit`}> + <Button + type="primary" + onClick={this.openToolClick} + style={{ marginLeft: 10 }} + > + {getIntlContent( + ruleList + ? ruleList.some( + (tool) => + toolSelectedRowKeys.includes(tool.id) && + tool.enabled, + ) + ? "SHENYU.PLUGIN.SELECTOR.BATCH.CLOSED" + : "SHENYU.PLUGIN.SELECTOR.BATCH.OPENED" + : "", + )} + </Button> + </AuthButton> + </div> + </div> + <Table + size="small" + style={{ marginTop: 30 }} + bordered + columns={toolsColumns} + expandedRowRender={expandedRowRender} + dataSource={ruleList} + rowSelection={toolRowSelection} + pagination={{ + total: toolTotal, + showTotal: (showTotal) => `${showTotal}`, + showSizeChanger: true, + pageSizeOptions: ["12", "20", "50", "100"], + current: toolPage, + pageSize: toolPageSize, + onChange: this.pageToolChange, + onShowSizeChange: this.pageToolChangeSize, + }} + /> + </Col> + </Row> + {popup} + </div> + ); + } +} diff --git a/src/services/api.js b/src/services/api.js index 2b6ad181..e3b34dd4 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1356,6 +1356,12 @@ export async function asyncNamespacePlugin(params) { }); } +/* get mcpServer list */ +export async function fetchMcpServer(params) { + return request(`${baseUrl}/mcpServer/list?${stringify(params)}`, { + method: `GET`, + }); +} /* getInstancesByNamespace */ export async function getInstancesByNamespace(params) { return request(`${baseUrl}/instance?${stringify(params)}`, { @@ -1363,6 +1369,33 @@ export async function getInstancesByNamespace(params) { }); } +/* add mcpServer */ +export async function addMcpServer(params) { + return request(`${baseUrl}/mcpServer/add`, { + method: `POST`, + body: { + ...params, + }, + }); +} + +/* update mcpServer */ +export async function updateMcpServer(params) { + return request(`${baseUrl}/mcpServer/update`, { + method: `PUT`, + body: { + ...params, + }, + }); +} + +/* delete mcpServer */ +export async function deleteMcpServer(params) { + return request(`${baseUrl}/mcpServer/delete`, { + method: `DELETE`, + body: [...params.list], + }); +} /* findInstance */ export async function findInstance(params) { return request(`${baseUrl}/instance/${params.id}`, {