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 591499b3 fix: guard optional discovery fields (#671)
591499b3 is described below

commit 591499b3150ec2c90f137f141112218548ba8fef
Author: Efe <[email protected]>
AuthorDate: Mon Sep 28 06:32:30 2026 +0300

    fix: guard optional discovery fields (#671)
    
    * fix: guard optional discovery fields
    
    Signed-off-by: Efe Gökdemir <[email protected]>
    
    * fix: guard missing discovery handlers during render
    
    Signed-off-by: Efe Gökdemir <[email protected]>
    
    * fix(discovery): satisfy lint for connected modal import
    
    Signed-off-by: Efe Gökdemir <[email protected]>
    
    ---------
    
    Signed-off-by: Efe Gökdemir <[email protected]>
---
 src/routes/Document/components/ApiInfo.js          |  5 +-
 .../Plugin/Discovery/DiscoveryConfigModal.js       |  5 +-
 src/routes/Plugin/Discovery/ProxySelectorModal.js  | 10 ++--
 .../Plugin/Discovery/ProxySelectorModal.test.js    | 58 ++++++++++++++++++++++
 src/routes/Plugin/Discovery/index.js               |  6 +--
 src/routes/Plugin/Discovery/optionalFields.js      | 28 +++++++++++
 src/routes/Plugin/Discovery/optionalFields.test.js | 44 ++++++++++++++++
 7 files changed, 145 insertions(+), 11 deletions(-)

diff --git a/src/routes/Document/components/ApiInfo.js 
b/src/routes/Document/components/ApiInfo.js
index 62f7f28e..dfdd8bf1 100644
--- a/src/routes/Document/components/ApiInfo.js
+++ b/src/routes/Document/components/ApiInfo.js
@@ -21,6 +21,7 @@ import ApiDebug from "./ApiDebug";
 import ApiContext from "./ApiContext";
 import { getIntlContent } from "../../../utils/IntlUtils";
 import { Method } from "./globalData";
+import { getLastTagName } from "../../Plugin/Discovery/optionalFields";
 
 const { Title, Text, Paragraph } = Typography;
 
@@ -159,9 +160,7 @@ function ApiInfo(props) {
       >
         <Row gutter={24}>
           <Col span={12}>
-            <Title level={2}>
-              {apiDetail.tags[apiDetail.tags.length - 1].name}
-            </Title>
+            <Title level={2}>{getLastTagName(apiDetail.tags)}</Title>
           </Col>
           <Col span={12} style={{ textAlign: "right" }}>
             <Button onClick={handleUpdate}>
diff --git a/src/routes/Plugin/Discovery/DiscoveryConfigModal.js 
b/src/routes/Plugin/Discovery/DiscoveryConfigModal.js
index 3514e0b6..4466b35a 100644
--- a/src/routes/Plugin/Discovery/DiscoveryConfigModal.js
+++ b/src/routes/Plugin/Discovery/DiscoveryConfigModal.js
@@ -19,6 +19,7 @@ import React, { Component } from "react";
 import { Button, Col, Form, Input, Modal, Popconfirm, Row, Select } from 
"antd";
 import { connect } from "dva";
 import { getIntlContent } from "../../../utils/IntlUtils";
+import { getDiscoveryProps } from "./optionalFields";
 
 const FormItem = Form.Item;
 
@@ -50,7 +51,9 @@ class DiscoveryConfigModal extends Component {
         },
       });
     } else {
-      this.setState({ configPropsJson: JSON.parse(data.props) });
+      this.setState({
+        configPropsJson: getDiscoveryProps(JSON.parse(data?.props || "{}")),
+      });
     }
   }
 
diff --git a/src/routes/Plugin/Discovery/ProxySelectorModal.js 
b/src/routes/Plugin/Discovery/ProxySelectorModal.js
index 62ce971c..63c5242e 100644
--- a/src/routes/Plugin/Discovery/ProxySelectorModal.js
+++ b/src/routes/Plugin/Discovery/ProxySelectorModal.js
@@ -36,6 +36,7 @@ import styles from "../index.less";
 import ProxySelectorCopy from "./ProxySelectorCopy.js";
 import { findKeyByValue } from "../../../utils/utils";
 import EditableFormTable from "./DiscoveryUpstreamTable";
+import { getDefaultValueList, getDiscoveryProps } from "./optionalFields";
 
 const FormItem = Form.Item;
 const { TabPane } = Tabs;
@@ -60,7 +61,7 @@ export class ProxySelectorModalComponent extends Component {
     const { isAdd, isSetConfig, discoveryType, data, pluginId, dispatch } =
       this.props;
     const { discoveryDicts } = this.state;
-    const { props } = this.props.data || {};
+    const props = getDiscoveryProps(this.props.data?.props);
 
     if (!isAdd || isSetConfig) {
       this.setState({
@@ -109,8 +110,9 @@ export class ProxySelectorModalComponent extends Component {
             pluginHandles[0] = filteredArray;
             this.setState({ pluginHandleList: pluginHandles });
 
-            let defaultValue = handlerArray[0].defaultValue;
-            this.setState({ defaultValueList: defaultValue.split(",") });
+            this.setState({
+              defaultValueList: getDefaultValueList(handlerArray[0]),
+            });
           }
         },
       },
@@ -607,7 +609,7 @@ export class ProxySelectorModalComponent extends Component {
                           style={{ width: "100%" }}
                         >
                           {(() => {
-                            if (discoveryHandler != null) {
+                            if (discoveryHandler?.[0] != null) {
                               let item = discoveryHandler[0];
                               let checkRule = item.checkRule;
                               let required = item.required === "1";
diff --git a/src/routes/Plugin/Discovery/ProxySelectorModal.test.js 
b/src/routes/Plugin/Discovery/ProxySelectorModal.test.js
new file mode 100644
index 00000000..7f97041b
--- /dev/null
+++ b/src/routes/Plugin/Discovery/ProxySelectorModal.test.js
@@ -0,0 +1,58 @@
+/*
+ * 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 from "react";
+import { render } from "@testing-library/react";
+
+jest.mock("./ProxySelectorCopy.js", () => () => null);
+jest.mock("./DiscoveryUpstreamTable", () => () => null);
+
+const { ProxySelectorModalComponent } = require("./ProxySelectorModal");
+
+describe("ProxySelectorModal", () => {
+  it("renders when another plugin handle exists without a discovery handler", 
() => {
+    const props = {
+      chosenType: "zookeeper",
+      data: {
+        discovery: { props: "{}", serverList: [] },
+        handler: "{}",
+      },
+      discoveryDicts: [],
+      discoveryType: "zookeeper",
+      typeEnums: ["local", "zookeeper"],
+      dispatch: ({ payload }) => {
+        if (payload.callBack) {
+          payload.callBack([[{ field: "loadBalance", dataType: 1 }]]);
+        }
+      },
+      form: {
+        getFieldDecorator: () => (element) => element,
+      },
+      handleCancel: jest.fn(),
+      handleOk: jest.fn(),
+      isAdd: true,
+      isSetConfig: false,
+      pluginId: "plugin-id",
+      recordCount: 0,
+      discoveryUpstreams: [],
+    };
+
+    expect(() =>
+      render(<ProxySelectorModalComponent {...props} />),
+    ).not.toThrow();
+  });
+});
diff --git a/src/routes/Plugin/Discovery/index.js 
b/src/routes/Plugin/Discovery/index.js
index 3484a538..30390703 100644
--- a/src/routes/Plugin/Discovery/index.js
+++ b/src/routes/Plugin/Discovery/index.js
@@ -30,7 +30,7 @@ import {
 import { getIntlContent } from "../../../utils/IntlUtils";
 import discoveryStyles from "./discovery.less";
 import DiscoveryConfigModal from "./DiscoveryConfigModal";
-import ProxySelectorModal from "./ProxySelectorModal";
+import ConnectedProxySelectorModal from "./ProxySelectorModal";
 import { DiscoveryCard } from "./DiscoveryCard";
 import AuthButton from "../../../utils/AuthButton";
 import { getUpdateModal, updatePluginsEnabled } from "../../../utils/plugin";
@@ -308,7 +308,7 @@ export default class DiscoveryProxy extends Component {
         }
         this.setState({
           popup: (
-            <ProxySelectorModal
+            <ConnectedProxySelectorModal
               pluginId={plugin.id}
               recordCount={cardData.discoveryUpstreams.length}
               typeEnums={typeEnums}
@@ -420,7 +420,7 @@ export default class DiscoveryProxy extends Component {
     });
     this.setState({
       popup: (
-        <ProxySelectorModal
+        <ConnectedProxySelectorModal
           recordCount={updateArray.length}
           discoveryUpstreams={updateArray}
           discoveryType={data.discovery.type}
diff --git a/src/routes/Plugin/Discovery/optionalFields.js 
b/src/routes/Plugin/Discovery/optionalFields.js
new file mode 100644
index 00000000..a810ed35
--- /dev/null
+++ b/src/routes/Plugin/Discovery/optionalFields.js
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+export function getLastTagName(tags) {
+  return tags?.[tags.length - 1]?.name;
+}
+
+export function getDiscoveryProps(props) {
+  return props || {};
+}
+
+export function getDefaultValueList(handler) {
+  return handler?.defaultValue ? handler.defaultValue.split(",") : [];
+}
diff --git a/src/routes/Plugin/Discovery/optionalFields.test.js 
b/src/routes/Plugin/Discovery/optionalFields.test.js
new file mode 100644
index 00000000..e68481c3
--- /dev/null
+++ b/src/routes/Plugin/Discovery/optionalFields.test.js
@@ -0,0 +1,44 @@
+/*
+ * 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 {
+  getDefaultValueList,
+  getDiscoveryProps,
+  getLastTagName,
+} from "./optionalFields";
+
+describe("optional discovery and document fields", () => {
+  it("returns the final tag name without assuming tags exist", () => {
+    expect(getLastTagName([{ name: "first" }, { name: "last" }])).toBe("last");
+    expect(getLastTagName([])).toBeUndefined();
+    expect(getLastTagName(undefined)).toBeUndefined();
+  });
+
+  it("normalizes missing discovery props to an empty object", () => {
+    expect(getDiscoveryProps(null)).toEqual({});
+    expect(getDiscoveryProps({ timeout: "3s" })).toEqual({ timeout: "3s" });
+  });
+
+  it("does not dereference a missing discovery handler", () => {
+    expect(getDefaultValueList(undefined)).toEqual([]);
+    expect(getDefaultValueList({})).toEqual([]);
+    expect(getDefaultValueList({ defaultValue: "http,https" })).toEqual([
+      "http",
+      "https",
+    ]);
+  });
+});

Reply via email to