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}`, {

Reply via email to