This is an automated email from the ASF dual-hosted git repository.
dengliming 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 7b08df1b fix: refresh MCP selectors after Swagger import (#691)
7b08df1b is described below
commit 7b08df1bcee5c24862f3ab318a33ccf34ca5cac9
Author: Carlos Acosta <[email protected]>
AuthorDate: Sun Sep 27 22:33:50 2026 -0500
fix: refresh MCP selectors after Swagger import (#691)
* fix: refresh MCP selectors after Swagger import
Co-authored-by: Aeontra <aeontra@localhost>
* fix: align MCP import tool pagination
Co-authored-by: Aeontra <aeontra@localhost>
* style: format selector refresh call
Co-authored-by: Aeontra <aeontra@localhost>
* fix: guard Swagger refresh plugin context
Co-authored-by: Aeontra <aeontra@localhost>
---------
Co-authored-by: Aeontra <aeontra@localhost>
---
src/models/common.js | 9 +-
src/models/common.pagination.test.js | 82 +++++++++++
src/models/mcpServer.js | 103 +++-----------
src/models/mcpServer.test.js | 95 +++++++++++++
src/routes/Plugin/McpServer/index.js | 29 +++-
src/routes/Plugin/McpServer/swaggerImport.test.js | 165 ++++++++++++++++++++++
src/services/api.js | 33 -----
7 files changed, 399 insertions(+), 117 deletions(-)
diff --git a/src/models/common.js b/src/models/common.js
index fff87f26..d06422e8 100644
--- a/src/models/common.js
+++ b/src/models/common.js
@@ -48,8 +48,11 @@ export default {
effects: {
*fetchSelector({ payload }, { call, put }) {
- const { namespaceId } = payload;
- const json = yield call(getAllSelectors, { ...payload });
+ const { namespaceId, rulePageSize = 12, ...selectorPayload } = payload;
+ const json = yield call(getAllSelectors, {
+ ...selectorPayload,
+ namespaceId,
+ });
if (json.code === 200) {
let { page, dataList } = json.data;
dataList = dataList.map((item) => {
@@ -75,7 +78,7 @@ export default {
type: "fetchRule",
payload: {
currentPage: 1,
- pageSize: 12,
+ pageSize: rulePageSize,
selectorId: dataList[0].id,
namespaceId,
},
diff --git a/src/models/common.pagination.test.js
b/src/models/common.pagination.test.js
new file mode 100644
index 00000000..b9ad5d19
--- /dev/null
+++ b/src/models/common.pagination.test.js
@@ -0,0 +1,82 @@
+/*
+ * 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 { runSaga, effects } from "dva/saga";
+import { getAllSelectors } from "../services/api";
+import model from "./common";
+
+jest.mock("../services/api", () => ({
+ getAllSelectors: jest.fn(),
+}));
+jest.mock("antd", () => ({
+ message: {
+ error: jest.fn(),
+ success: jest.fn(),
+ warn: jest.fn(),
+ },
+}));
+jest.mock("../utils/IntlUtils", () => ({
+ getIntlContent: (key) => key,
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it("keeps the tool pager aligned with the selector refresh", async () => {
+ getAllSelectors.mockResolvedValue({
+ code: 200,
+ data: {
+ page: { totalCount: 1 },
+ dataList: [{ id: "selector-1", name: "orders" }],
+ },
+ });
+ const actions = [];
+
+ await runSaga(
+ { dispatch: (action) => actions.push(action), logger: jest.fn() },
+ model.effects.fetchSelector,
+ {
+ payload: {
+ pluginId: "plugin-1",
+ currentPage: 3,
+ pageSize: 20,
+ name: "orders",
+ namespaceId: "namespace-1",
+ rulePageSize: 50,
+ },
+ },
+ effects,
+ ).done;
+
+ expect(getAllSelectors).toHaveBeenCalledWith({
+ pluginId: "plugin-1",
+ currentPage: 3,
+ pageSize: 20,
+ name: "orders",
+ namespaceId: "namespace-1",
+ });
+ expect(actions).toContainEqual({
+ type: "fetchRule",
+ payload: {
+ currentPage: 1,
+ pageSize: 50,
+ selectorId: "selector-1",
+ namespaceId: "namespace-1",
+ },
+ });
+});
diff --git a/src/models/mcpServer.js b/src/models/mcpServer.js
index 65b89256..970f2dab 100644
--- a/src/models/mcpServer.js
+++ b/src/models/mcpServer.js
@@ -1,96 +1,39 @@
+/*
+ * 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 { message } from "antd";
-import {
- fetchMcpServer,
- addMcpServer,
- updateMcpServer,
- deleteMcpServer,
- mcpSwaggerImport,
-} from "../services/api";
-import { getIntlContent } from "../utils/IntlUtils";
+import { mcpSwaggerImport } from "../services/api";
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 },
- });
- },
- *swaggerImport({ payload, callback }, { call, put }) {
+ *swaggerImport({ payload, fetchValue, callback }, { call, put }) {
const json = yield call(mcpSwaggerImport, payload);
if (json.code === 200) {
message.success(json.message);
- yield put({ type: "reload" });
+ yield put({
+ type: "common/fetchSelector",
+ payload: fetchValue,
+ });
if (callback) callback();
} else {
message.warn(json.message);
}
},
},
-
- reducers: {
- saveList(state, { payload }) {
- return {
- ...state,
- list: payload.list,
- total: payload.total,
- currentPage: payload.currentPage,
- pageSize: payload.pageSize,
- };
- },
- },
};
diff --git a/src/models/mcpServer.test.js b/src/models/mcpServer.test.js
new file mode 100644
index 00000000..53c05d79
--- /dev/null
+++ b/src/models/mcpServer.test.js
@@ -0,0 +1,95 @@
+/*
+ * 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 { message } from "antd";
+import { mcpSwaggerImport } from "../services/api";
+import model from "./mcpServer";
+
+jest.mock("antd", () => ({
+ message: {
+ success: jest.fn(),
+ warn: jest.fn(),
+ },
+}));
+
+jest.mock("../services/api", () => ({
+ mcpSwaggerImport: jest.fn(),
+}));
+
+const call = (fn, payload) => ({ type: "call", fn, payload });
+const put = (action) => ({ type: "put", action });
+const effects = { call, put };
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it("refreshes the visible selector list after a successful Swagger import", ()
=> {
+ const payload = {
+ swaggerUrl: "https://example.test/openapi.json",
+ projectName: "example",
+ namespaceId: "namespace-1",
+ };
+ const fetchValue = {
+ pluginId: "plugin-1",
+ currentPage: 2,
+ pageSize: 20,
+ name: "orders",
+ namespaceId: "namespace-1",
+ };
+ const callback = jest.fn();
+ const generator = model.effects.swaggerImport(
+ { payload, fetchValue, callback },
+ effects,
+ );
+
+ expect(generator.next().value).toEqual(call(mcpSwaggerImport, payload));
+
+ const response = { code: 200, message: "Imported" };
+ expect(generator.next(response).value).toEqual(
+ put({
+ type: "common/fetchSelector",
+ payload: fetchValue,
+ }),
+ );
+ expect(message.success).toHaveBeenCalledWith(response.message);
+ expect(callback).not.toHaveBeenCalled();
+
+ expect(generator.next().done).toBe(true);
+ expect(callback).toHaveBeenCalledTimes(1);
+ expect(message.warn).not.toHaveBeenCalled();
+});
+
+it("does not refresh or close the modal after a failed Swagger import", () => {
+ const payload = {
+ swaggerUrl: "https://example.test/openapi.json",
+ projectName: "example",
+ namespaceId: "namespace-1",
+ };
+ const callback = jest.fn();
+ const generator = model.effects.swaggerImport(
+ { payload, fetchValue: {}, callback },
+ effects,
+ );
+
+ generator.next();
+ const response = { code: 500, message: "Import failed" };
+ expect(generator.next(response).done).toBe(true);
+ expect(message.warn).toHaveBeenCalledWith(response.message);
+ expect(message.success).not.toHaveBeenCalled();
+ expect(callback).not.toHaveBeenCalled();
+});
diff --git a/src/routes/Plugin/McpServer/index.js
b/src/routes/Plugin/McpServer/index.js
index ff01cdde..ae78abc6 100755
--- a/src/routes/Plugin/McpServer/index.js
+++ b/src/routes/Plugin/McpServer/index.js
@@ -421,11 +421,29 @@ export default class McpServer extends Component {
};
swaggerImportClick = () => {
- const { dispatch, currentNamespaceId } = this.props;
+ const { dispatch, currentNamespaceId, plugins } = this.props;
+ const {
+ pluginName,
+ selectorName,
+ selectorPage,
+ selectorPageSize,
+ toolPageSize,
+ } = this.state;
+ const pluginId = this.getPlugin(plugins || [], pluginName)?.pluginId;
+ if (!pluginId) {
+ return;
+ }
this.setState({
popup: (
<SwaggerImportModal
handleOk={(values) => {
+ const currentPluginId = this.getPlugin(
+ this.props.plugins || [],
+ pluginName,
+ )?.pluginId;
+ if (!currentPluginId) {
+ return;
+ }
const { swaggerUrl, projectName } = values;
dispatch({
type: "mcpServer/swaggerImport",
@@ -434,7 +452,16 @@ export default class McpServer extends Component {
projectName,
namespaceId: currentNamespaceId,
},
+ fetchValue: {
+ pluginId: currentPluginId,
+ currentPage: selectorPage,
+ pageSize: selectorPageSize,
+ name: selectorName,
+ namespaceId: currentNamespaceId,
+ rulePageSize: toolPageSize,
+ },
callback: () => {
+ this.setState({ toolPage: 1 });
this.closeModal();
},
});
diff --git a/src/routes/Plugin/McpServer/swaggerImport.test.js
b/src/routes/Plugin/McpServer/swaggerImport.test.js
new file mode 100644
index 00000000..18b40a3e
--- /dev/null
+++ b/src/routes/Plugin/McpServer/swaggerImport.test.js
@@ -0,0 +1,165 @@
+/*
+ * 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 ConnectedMcpServer from "./index";
+
+jest.mock("react-json-view", () => () => null);
+jest.mock("../Common/Selector", () => () => null);
+jest.mock("./ToolsModal", () => () => null);
+jest.mock("./JsonEditModal", () => () => null);
+jest.mock("./McpConfigModal", () => () => null);
+jest.mock("./SwaggerImportModal", () => () => null);
+jest.mock(
+ "../../../utils/AuthButton",
+ () =>
+ ({ children }) =>
+ children,
+);
+jest.mock("../../../utils/IntlUtils", () => ({
+ getCurrentLocale: jest.fn(),
+ getIntlContent: (key) => key,
+}));
+jest.mock("../../../utils/namespacePlugin", () => ({
+ getUpdateModal: jest.fn(),
+ updateNamespacePluginsEnabledByNamespace: jest.fn(),
+}));
+
+const McpServer = ConnectedMcpServer.WrappedComponent;
+
+it("passes the active selector context to Swagger import refresh", () => {
+ const dispatch = jest.fn();
+ const component = new McpServer({
+ dispatch,
+ currentNamespaceId: "namespace-1",
+ plugins: [
+ {
+ name: "mcpServer",
+ pluginId: "plugin-1",
+ },
+ ],
+ });
+
+ component.state = {
+ ...component.state,
+ selectorPage: 3,
+ selectorPageSize: 20,
+ selectorName: "orders",
+ toolPage: 3,
+ toolPageSize: 50,
+ };
+ component.setState = (update) => {
+ const next =
+ typeof update === "function"
+ ? update(component.state, component.props)
+ : update;
+ component.state = { ...component.state, ...next };
+ };
+
+ component.swaggerImportClick();
+ component.state.popup.props.handleOk({
+ swaggerUrl: "https://example.test/openapi.json",
+ projectName: "example",
+ });
+
+ expect(dispatch).toHaveBeenCalledWith({
+ type: "mcpServer/swaggerImport",
+ payload: {
+ swaggerUrl: "https://example.test/openapi.json",
+ projectName: "example",
+ namespaceId: "namespace-1",
+ },
+ fetchValue: {
+ pluginId: "plugin-1",
+ currentPage: 3,
+ pageSize: 20,
+ name: "orders",
+ namespaceId: "namespace-1",
+ rulePageSize: 50,
+ },
+ callback: expect.any(Function),
+ });
+
+ dispatch.mock.calls[0][0].callback();
+ expect(component.state.popup).toBe("");
+ expect(component.state.toolPage).toBe(1);
+ expect(component.state.toolPageSize).toBe(50);
+});
+
+it("does not open Swagger import before plugin metadata is available", () => {
+ const dispatch = jest.fn();
+ const component = new McpServer({
+ dispatch,
+ currentNamespaceId: "namespace-1",
+ plugins: [],
+ });
+ component.setState = (update) => {
+ const next =
+ typeof update === "function"
+ ? update(component.state, component.props)
+ : update;
+ component.state = { ...component.state, ...next };
+ };
+
+ component.swaggerImportClick();
+
+ expect(component.state.popup).toBe("");
+ expect(dispatch).not.toHaveBeenCalled();
+});
+
+it("revalidates the plugin id when the Swagger import is submitted", () => {
+ const dispatch = jest.fn();
+ const component = new McpServer({
+ dispatch,
+ currentNamespaceId: "namespace-1",
+ plugins: [
+ {
+ name: "mcpServer",
+ pluginId: "plugin-1",
+ },
+ ],
+ });
+ component.setState = (update) => {
+ const next =
+ typeof update === "function"
+ ? update(component.state, component.props)
+ : update;
+ component.state = { ...component.state, ...next };
+ };
+
+ component.swaggerImportClick();
+ component.props = {
+ ...component.props,
+ plugins: [
+ {
+ name: "mcpServer",
+ pluginId: "plugin-2",
+ },
+ ],
+ };
+ component.state.popup.props.handleOk({
+ swaggerUrl: "https://example.test/openapi.json",
+ projectName: "example",
+ });
+
+ expect(dispatch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ fetchValue: expect.objectContaining({
+ pluginId: "plugin-2",
+ }),
+ }),
+ );
+});
diff --git a/src/services/api.js b/src/services/api.js
index edadc785..135dc967 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -1367,12 +1367,6 @@ 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)}`, {
@@ -1380,33 +1374,6 @@ 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}`, {