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);