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,