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 af41dfd5 fix: guard proxy selector copy selection (#676)
af41dfd5 is described below

commit af41dfd5cd17de313eb9fa39034d6efc7f885357
Author: Carlos Acosta <[email protected]>
AuthorDate: Sun Sep 27 00:28:25 2026 -0500

    fix: guard proxy selector copy selection (#676)
    
    Co-authored-by: Aeontra <aeontra@localhost>
---
 src/routes/Plugin/Discovery/ProxySelectorCopy.js   | 21 ++++--
 .../Plugin/Discovery/ProxySelectorCopy.test.js     | 86 ++++++++++++++++++++++
 .../Discovery/ProxySelectorModal.copy.test.js      | 60 +++++++++++++++
 src/routes/Plugin/Discovery/ProxySelectorModal.js  | 22 ++++--
 4 files changed, 175 insertions(+), 14 deletions(-)

diff --git a/src/routes/Plugin/Discovery/ProxySelectorCopy.js 
b/src/routes/Plugin/Discovery/ProxySelectorCopy.js
index b2d598da..246ca63c 100644
--- a/src/routes/Plugin/Discovery/ProxySelectorCopy.js
+++ b/src/routes/Plugin/Discovery/ProxySelectorCopy.js
@@ -16,15 +16,12 @@
  */
 
 import React, { Component } from "react";
-import { Modal, Select } from "antd";
+import { message, Modal, Select } from "antd";
 import { connect } from "dva";
 import { fetchProxySelector } from "../../../services/api";
 import { getIntlContent } from "../../../utils/IntlUtils";
 
-@connect(({ global }) => ({
-  currentNamespaceId: global.currentNamespaceId,
-}))
-class ProxySelectorCopy extends Component {
+export class ProxySelectorCopyComponent extends Component {
   constructor(props) {
     super(props);
     this.state = {
@@ -61,6 +58,10 @@ class ProxySelectorCopy extends Component {
     this.setState({
       loading: false,
     });
+    if (!data) {
+      message.warn(getIntlContent("SHENYU.COMMON.WARN.INPUT_SELECTOR"));
+      return;
+    }
     // eslint-disable-next-line no-unused-expressions
     onOk && onOk(data);
   };
@@ -88,7 +89,10 @@ class ProxySelectorCopy extends Component {
 
   render() {
     const { visible = false, disabled } = this.props;
-    const { loading } = this.state;
+    const { loading, selectedValue, selectorList } = this.state;
+    const hasValidSelection = selectorList.some(
+      (selector) => selector.id === selectedValue,
+    );
     return (
       <Modal
         visible={visible}
@@ -97,6 +101,7 @@ class ProxySelectorCopy extends Component {
         onCancel={this.handleCancel}
         onOk={this.handleOk}
         confirmLoading={loading}
+        okButtonProps={{ disabled: !hasValidSelection }}
       >
         <Select
           disabled={disabled}
@@ -117,4 +122,6 @@ class ProxySelectorCopy extends Component {
   }
 }
 
-export default ProxySelectorCopy;
+export default connect(({ global }) => ({
+  currentNamespaceId: global.currentNamespaceId,
+}))(ProxySelectorCopyComponent);
diff --git a/src/routes/Plugin/Discovery/ProxySelectorCopy.test.js 
b/src/routes/Plugin/Discovery/ProxySelectorCopy.test.js
new file mode 100644
index 00000000..f0d4c584
--- /dev/null
+++ b/src/routes/Plugin/Discovery/ProxySelectorCopy.test.js
@@ -0,0 +1,86 @@
+/*
+ * 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 { ProxySelectorCopyComponent } from "./ProxySelectorCopy";
+
+jest.mock("../../../services/api", () => ({
+  fetchProxySelector: jest.fn(),
+}));
+jest.mock("../../../utils/IntlUtils", () => ({
+  getIntlContent: (key) => key,
+}));
+
+beforeEach(() => {
+  jest.spyOn(message, "warn").mockImplementation(() => {});
+});
+
+afterEach(() => {
+  jest.restoreAllMocks();
+});
+
+const makeComponent = (props = {}) => {
+  const component = new ProxySelectorCopyComponent(props);
+  component.setState = (update) => {
+    component.state = { ...component.state, ...update };
+  };
+  return component;
+};
+
+it("disables confirmation until a valid selector is selected", () => {
+  const component = makeComponent({});
+  component.state = {
+    ...component.state,
+    selectorList: [{ id: "selector-1", name: "Selector" }],
+  };
+
+  expect(component.render().props.okButtonProps.disabled).toBe(true);
+
+  component.state.selectedValue = "selector-1";
+  expect(component.render().props.okButtonProps.disabled).toBe(false);
+});
+
+it("does not call onOk when the selected selector is missing", async () => {
+  const onOk = jest.fn();
+  const component = makeComponent({ onOk });
+  component.state = {
+    ...component.state,
+    selectedValue: "missing",
+    selectorList: [{ id: "selector-1" }],
+  };
+
+  await component.handleOk();
+
+  expect(onOk).not.toHaveBeenCalled();
+  expect(message.warn).toHaveBeenCalled();
+  expect(component.state.loading).toBe(false);
+});
+
+it("passes the selected selector to onOk", async () => {
+  const onOk = jest.fn();
+  const selected = { id: "selector-1", discovery: { type: "local" } };
+  const component = makeComponent({ onOk });
+  component.state = {
+    ...component.state,
+    selectedValue: "selector-1",
+    selectorList: [selected],
+  };
+
+  await component.handleOk();
+
+  expect(onOk).toHaveBeenCalledWith(selected);
+});
diff --git a/src/routes/Plugin/Discovery/ProxySelectorModal.copy.test.js 
b/src/routes/Plugin/Discovery/ProxySelectorModal.copy.test.js
new file mode 100644
index 00000000..a9e38cd7
--- /dev/null
+++ b/src/routes/Plugin/Discovery/ProxySelectorModal.copy.test.js
@@ -0,0 +1,60 @@
+/*
+ * 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 { ProxySelectorModalComponent } from "./ProxySelectorModal";
+
+jest.mock("./ProxySelectorCopy.js", () => () => null);
+jest.mock("./DiscoveryUpstreamTable", () => () => null);
+jest.mock("../../../utils/IntlUtils", () => ({
+  getIntlContent: (key) => key,
+}));
+jest.mock("../../../utils/utils", () => ({
+  findKeyByValue: jest.fn(),
+}));
+
+beforeEach(() => {
+  jest.spyOn(message, "warn").mockImplementation(() => {});
+});
+
+afterEach(() => {
+  jest.restoreAllMocks();
+});
+
+it("ignores missing copy data instead of dereferencing it", () => {
+  const form = { setFieldsValue: jest.fn() };
+  const dispatch = jest.fn();
+  const component = new ProxySelectorModalComponent({ form, dispatch });
+
+  expect(() => component.handleCopyData(undefined)).not.toThrow();
+
+  expect(form.setFieldsValue).not.toHaveBeenCalled();
+  expect(dispatch).not.toHaveBeenCalled();
+  expect(message.warn).toHaveBeenCalled();
+});
+
+it("ignores copy data without discovery configuration", () => {
+  const form = { setFieldsValue: jest.fn() };
+  const dispatch = jest.fn();
+  const component = new ProxySelectorModalComponent({ form, dispatch });
+
+  component.handleCopyData({ id: "selector-1" });
+
+  expect(form.setFieldsValue).not.toHaveBeenCalled();
+  expect(dispatch).not.toHaveBeenCalled();
+  expect(message.warn).toHaveBeenCalled();
+});
diff --git a/src/routes/Plugin/Discovery/ProxySelectorModal.js 
b/src/routes/Plugin/Discovery/ProxySelectorModal.js
index 16f1f08f..62ce971c 100644
--- a/src/routes/Plugin/Discovery/ProxySelectorModal.js
+++ b/src/routes/Plugin/Discovery/ProxySelectorModal.js
@@ -23,6 +23,7 @@ import {
   Divider,
   Form,
   Input,
+  message,
   Modal,
   Row,
   Select,
@@ -40,12 +41,7 @@ const FormItem = Form.Item;
 const { TabPane } = Tabs;
 const { Option } = Select;
 
-@connect(({ discovery, pluginHandle, shenyuDict }) => ({
-  ...discovery,
-  ...pluginHandle,
-  ...shenyuDict,
-}))
-class ProxySelectorModal extends Component {
+export class ProxySelectorModalComponent extends Component {
   constructor(props) {
     super(props);
     this.state = {
@@ -179,6 +175,10 @@ class ProxySelectorModal extends Component {
 
   handleCopyData = (copyData) => {
     const { form, dispatch } = this.props;
+    if (!copyData || !copyData.discovery) {
+      message.warn(getIntlContent("SHENYU.COMMON.WARN.INPUT_SELECTOR"));
+      return;
+    }
     const { name, forwardPort, discovery, listenerNode } = copyData;
     const formData = {
       name,
@@ -783,4 +783,12 @@ class ProxySelectorModal extends Component {
   }
 }
 
-export default Form.create()(ProxySelectorModal);
+const ConnectedProxySelectorModal = connect(
+  ({ discovery, pluginHandle, shenyuDict }) => ({
+    ...discovery,
+    ...pluginHandle,
+    ...shenyuDict,
+  }),
+)(ProxySelectorModalComponent);
+
+export default Form.create()(ConnectedProxySelectorModal);

Reply via email to