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 e0e890bf fix: preserve passwords on credential edits (#682)
e0e890bf is described below

commit e0e890bf0d617c6efede746403094e1d68137cd7
Author: Carlos Acosta <[email protected]>
AuthorDate: Sun Sep 27 00:32:24 2026 -0500

    fix: preserve passwords on credential edits (#682)
    
    Co-authored-by: Aeontra <aeontra@localhost>
---
 src/routes/System/Registry/AddModal.js      |  26 ++++---
 src/routes/System/Registry/AddModal.test.js |  89 ++++++++++++++++++++++++
 src/routes/System/User/AddModal.js          |  22 ++++--
 src/routes/System/User/AddModal.test.js     | 103 ++++++++++++++++++++++++++++
 src/routes/System/User/index.js             |   9 +--
 5 files changed, 224 insertions(+), 25 deletions(-)

diff --git a/src/routes/System/Registry/AddModal.js 
b/src/routes/System/Registry/AddModal.js
index edd2c393..ac79b8a1 100755
--- a/src/routes/System/Registry/AddModal.js
+++ b/src/routes/System/Registry/AddModal.js
@@ -21,7 +21,7 @@ import { getIntlContent } from "../../../utils/IntlUtils";
 
 const FormItem = Form.Item;
 
-class AddModal extends Component {
+export class RegistryAddModal extends Component {
   handleSubmit = (e) => {
     const { form, handleOk } = this.props;
     const { id } = this.props?.detail || {};
@@ -37,7 +37,7 @@ class AddModal extends Component {
           namespace,
           group,
         } = values;
-        handleOk({
+        const submitValues = {
           registryId,
           protocol,
           address,
@@ -46,7 +46,16 @@ class AddModal extends Component {
           namespace,
           group,
           id,
-        });
+        };
+        if (
+          id &&
+          (submitValues.password === "" ||
+            submitValues.password === undefined ||
+            submitValues.password === null)
+        ) {
+          delete submitValues.password;
+        }
+        handleOk(submitValues);
       }
     });
   };
@@ -59,7 +68,6 @@ class AddModal extends Component {
       protocol = "",
       address = "",
       username = "",
-      password = "",
       namespace = "",
       group = "",
       id = "",
@@ -108,7 +116,7 @@ class AddModal extends Component {
                 placeholder={getIntlContent(
                   "SHENYU.REGISTRY.REGISTRY_ID.INPUT",
                 )}
-                disabled={id}
+                disabled={Boolean(id)}
               />,
             )}
           </FormItem>
@@ -169,10 +177,8 @@ class AddModal extends Component {
             label={getIntlContent("SHENYU.REGISTRY.PASSPORT")}
             {...formItemLayout}
           >
-            {getFieldDecorator("password", {
-              initialValue: password,
-            })(
-              <Input
+            {getFieldDecorator("password")(
+              <Input.Password
                 placeholder={getIntlContent("SHENYU.REGISTRY.PASSPORT.INPUT")}
               />,
             )}
@@ -213,4 +219,4 @@ class AddModal extends Component {
   }
 }
 
-export default Form.create()(AddModal);
+export default Form.create()(RegistryAddModal);
diff --git a/src/routes/System/Registry/AddModal.test.js 
b/src/routes/System/Registry/AddModal.test.js
new file mode 100644
index 00000000..1826ea71
--- /dev/null
+++ b/src/routes/System/Registry/AddModal.test.js
@@ -0,0 +1,89 @@
+/*
+ * 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 { Input } from "antd";
+import { RegistryAddModal } from "./AddModal";
+
+jest.mock("../../../utils/IntlUtils", () => ({
+  getIntlContent: (key) => key,
+}));
+
+const baseValues = {
+  registryId: "registry-1",
+  protocol: "zookeeper",
+  address: "127.0.0.1:2181",
+  username: "alice",
+  namespace: "namespace",
+  group: "group",
+};
+const submitForm = (values) => ({
+  validateFieldsAndScroll: (callback) => callback(null, values),
+});
+const renderForm = () => ({
+  getFieldDecorator:
+    (name, options = {}) =>
+    (element) =>
+      React.cloneElement(element, {
+        "data-field-name": name,
+        "data-field-options": options,
+      }),
+});
+const findField = (node, name) => {
+  if (!React.isValidElement(node)) return null;
+  if (node.props["data-field-name"] === name) return node;
+  return React.Children.toArray(node.props.children).reduce(
+    (match, child) => match || findField(child, name),
+    null,
+  );
+};
+
+it("omits an untouched password when editing a registry", () => {
+  const handleOk = jest.fn();
+  const component = new RegistryAddModal({
+    detail: { id: "registry-row-1" },
+    handleOk,
+    form: submitForm({ ...baseValues, password: "" }),
+  });
+  component.handleSubmit({ preventDefault: jest.fn() });
+  expect(handleOk).toHaveBeenCalledWith({
+    ...baseValues,
+    id: "registry-row-1",
+  });
+});
+
+it("submits an explicitly entered registry replacement password", () => {
+  const handleOk = jest.fn();
+  const component = new RegistryAddModal({
+    detail: { id: "registry-row-1" },
+    handleOk,
+    form: submitForm({ ...baseValues, password: "replacement" }),
+  });
+  component.handleSubmit({ preventDefault: jest.fn() });
+  expect(handleOk).toHaveBeenCalledWith({
+    ...baseValues,
+    password: "replacement",
+    id: "registry-row-1",
+  });
+});
+
+it("renders registry passwords with a masked input", () => {
+  const component = new RegistryAddModal({
+    detail: {},
+    form: renderForm(),
+  });
+  expect(findField(component.render(), "password").type).toBe(Input.Password);
+});
diff --git a/src/routes/System/User/AddModal.js 
b/src/routes/System/User/AddModal.js
index cdd11525..d1b3d524 100644
--- a/src/routes/System/User/AddModal.js
+++ b/src/routes/System/User/AddModal.js
@@ -22,13 +22,22 @@ import { getIntlContent } from "../../../utils/IntlUtils";
 const FormItem = Form.Item;
 const { Option } = Select;
 
-class AddModal extends Component {
+export class UserAddModal extends Component {
   handleSubmit = (e) => {
     const { form, handleOk, id = "" } = this.props;
     e.preventDefault();
     form.validateFieldsAndScroll((err, values) => {
       if (!err) {
-        handleOk({ ...values, id });
+        const submitValues = { ...values, id };
+        if (
+          id &&
+          (submitValues.password === "" ||
+            submitValues.password === undefined ||
+            submitValues.password === null)
+        ) {
+          delete submitValues.password;
+        }
+        handleOk(submitValues);
       }
     });
   };
@@ -38,10 +47,10 @@ class AddModal extends Component {
       handleCancel,
       form,
       userName = "",
-      password = "",
       roles = [],
       enabled = true,
       allRoles = [],
+      id = "",
     } = this.props;
 
     const { getFieldDecorator } = form;
@@ -92,13 +101,12 @@ class AddModal extends Component {
             {getFieldDecorator("password", {
               rules: [
                 {
-                  required: true,
+                  required: !id,
                   message: getIntlContent("SHENYU.SYSTEM.USER.PASSWORD"),
                 },
               ],
-              initialValue: password,
             })(
-              <Input
+              <Input.Password
                 allowClear
                 placeholder={getIntlContent("SHENYU.SYSTEM.PASSWORD")}
               />,
@@ -145,4 +153,4 @@ class AddModal extends Component {
   }
 }
 
-export default Form.create()(AddModal);
+export default Form.create()(UserAddModal);
diff --git a/src/routes/System/User/AddModal.test.js 
b/src/routes/System/User/AddModal.test.js
new file mode 100644
index 00000000..de0b9fde
--- /dev/null
+++ b/src/routes/System/User/AddModal.test.js
@@ -0,0 +1,103 @@
+/*
+ * 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 { Input } from "antd";
+import { UserAddModal } from "./AddModal";
+
+jest.mock("../../../utils/IntlUtils", () => ({
+  getIntlContent: (key) => key,
+}));
+
+const submitForm = (values) => ({
+  validateFieldsAndScroll: (callback) => callback(null, values),
+});
+const renderForm = () => ({
+  getFieldDecorator:
+    (name, options = {}) =>
+    (element) =>
+      React.cloneElement(element, {
+        "data-field-name": name,
+        "data-field-options": options,
+      }),
+});
+const findField = (node, name) => {
+  if (!React.isValidElement(node)) return null;
+  if (node.props["data-field-name"] === name) return node;
+  return React.Children.toArray(node.props.children).reduce(
+    (match, child) => match || findField(child, name),
+    null,
+  );
+};
+
+it("omits an untouched password when editing a user", () => {
+  const handleOk = jest.fn();
+  const component = new UserAddModal({
+    id: "user-1",
+    handleOk,
+    form: submitForm({
+      userName: "alice",
+      password: "",
+      roles: ["role-1"],
+      enabled: true,
+    }),
+  });
+  component.handleSubmit({ preventDefault: jest.fn() });
+  expect(handleOk).toHaveBeenCalledWith({
+    userName: "alice",
+    roles: ["role-1"],
+    enabled: true,
+    id: "user-1",
+  });
+});
+
+it("submits an explicitly entered replacement password", () => {
+  const handleOk = jest.fn();
+  const component = new UserAddModal({
+    id: "user-1",
+    handleOk,
+    form: submitForm({
+      userName: "alice",
+      password: "replacement",
+      roles: ["role-1"],
+      enabled: true,
+    }),
+  });
+  component.handleSubmit({ preventDefault: jest.fn() });
+  expect(handleOk).toHaveBeenCalledWith(
+    expect.objectContaining({ id: "user-1", password: "replacement" }),
+  );
+});
+
+it("masks the password and requires it only on create", () => {
+  const create = new UserAddModal({ form: renderForm(), allRoles: [] });
+  const edit = new UserAddModal({
+    id: "user-1",
+    form: renderForm(),
+    allRoles: [],
+  });
+  const createPassword = findField(create.render(), "password");
+  const editPassword = findField(edit.render(), "password");
+
+  expect(createPassword.type).toBe(Input.Password);
+  expect(editPassword.type).toBe(Input.Password);
+  expect(createPassword.props["data-field-options"].rules[0].required).toBe(
+    true,
+  );
+  expect(editPassword.props["data-field-options"].rules[0].required).toBe(
+    false,
+  );
+});
diff --git a/src/routes/System/User/index.js b/src/routes/System/User/index.js
index 927ded4d..ad839961 100644
--- a/src/routes/System/User/index.js
+++ b/src/routes/System/User/index.js
@@ -106,16 +106,9 @@ export default class Manage extends Component {
               {...user}
               allRoles={allRoles}
               handleOk={(values) => {
-                const { userName, password, roles, enabled, id } = values;
                 dispatch({
                   type: "manage/update",
-                  payload: {
-                    userName,
-                    password,
-                    roles,
-                    enabled,
-                    id,
-                  },
+                  payload: values,
                   fetchValue: {
                     userName: name,
                     currentPage,

Reply via email to