This is an automated email from the ASF dual-hosted git repository.

JiaLiangC pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 474f6711c1 AMBARI-26648: Service Configs Bug: all properties render 
read-only, and JVM/command-line values incorrectly split into multi-line 
display (#4205)
474f6711c1 is described below

commit 474f6711c18663c29c0fc674053d21e7adcc2295
Author: Sandeep  Kumar <[email protected]>
AuthorDate: Mon Sep 7 12:23:16 2026 +0530

    AMBARI-26648: Service Configs Bug: all properties render read-only, and 
JVM/command-line values incorrectly split into multi-line display (#4205)
---
 ambari-web/latest/src/Utils/configHistory.ts       |   6 +-
 ambari-web/latest/src/Utils/jvmFormatUtils.test.ts |  39 +++++---
 ambari-web/latest/src/Utils/jvmFormatUtils.ts      | 104 ++++++++++-----------
 .../latest/src/screens/ServiceConfigs/index.tsx    |   3 +-
 4 files changed, 81 insertions(+), 71 deletions(-)

diff --git a/ambari-web/latest/src/Utils/configHistory.ts 
b/ambari-web/latest/src/Utils/configHistory.ts
index 69ba38ba3f..1616402cb4 100644
--- a/ambari-web/latest/src/Utils/configHistory.ts
+++ b/ambari-web/latest/src/Utils/configHistory.ts
@@ -113,7 +113,11 @@ export function resolveConfigHistorySelection(
   currentDefaultVersion: string,
   navigationState?: ConfigHistoryNavigationState | null,
 ) {
-  const selectedVersion = navigationState?.serviceConfigVersion || 
currentDefaultVersion;
+  // Normalize to string: navigationState may carry a raw numeric version
+  // (e.g. from router state constructed elsewhere), and comparing that
+  // against the string-typed currentDefaultVersion with === would otherwise
+  // never match, making every property look read-only.
+  const selectedVersion = String(navigationState?.serviceConfigVersion || 
currentDefaultVersion);
   const configGroup = navigationState?.configGroup || "Default";
   const versionsToLoad = selectedVersion === currentDefaultVersion && 
configGroup === "Default"
     ? null
diff --git a/ambari-web/latest/src/Utils/jvmFormatUtils.test.ts 
b/ambari-web/latest/src/Utils/jvmFormatUtils.test.ts
index 27e1230ec1..6cf4c0e330 100644
--- a/ambari-web/latest/src/Utils/jvmFormatUtils.test.ts
+++ b/ambari-web/latest/src/Utils/jvmFormatUtils.test.ts
@@ -24,24 +24,33 @@ import {
 } from "./jvmFormatUtils";
 
 describe("JVM parameter formatting", () => {
-  it("formats JVM options without splitting quoted whitespace", () => {
-    const value = '-Xmx1g -Dname="value with spaces" -XX:+UseG1GC';
-    const displayed = [
-      "-Xmx1g",
-      '-Dname="value with spaces"',
-      "-XX:+UseG1GC",
-    ].join("\n");
+  it("does not treat single-line JVM/command-line argument strings as 
multiline", () => {
+    // e.g. mapreduce.admin.map.child.java.opts — must stay single-line, or the
+    // injected display newlines get persisted and corrupt the config.
+    const value =
+      "-server -XX:NewRatio=8 -Djava.net.preferIPv4Stack=true 
-Dhdp.version=${hdp.version}";
 
-    expect(shouldUseMultilineFormatting(value, "string")).toBe(true);
-    expect(formatParamsForDisplay(value, "string")).toBe(displayed);
-    expect(formatParamsForSave(displayed)).toBe(value);
+    expect(shouldUseMultilineFormatting(value)).toBe(false);
+    expect(formatParamsForDisplay(value)).toBe(value);
+    expect(formatParamsForSave(value)).toBe(value);
   });
 
-  it("does not rewrite ordinary or explicitly multiline content", () => {
+  it("honors an explicit multiLine displayType regardless of content", () => {
+    expect(shouldUseMultilineFormatting("-Xmx1g -Xms1g", 
"multiLine")).toBe(true);
+  });
+
+  it("does not treat content as multiline when displayType says otherwise", () 
=> {
     expect(shouldUseMultilineFormatting("first\nsecond", 
"string")).toBe(false);
-    expect(shouldUseMultilineFormatting("-Xmx1g -Xms1g", "multiLine")).toBe(
-      false,
-    );
-    expect(shouldUseMultilineFormatting("plain words", "string")).toBe(false);
+  });
+
+  it("falls back to content-based detection when no displayType is given", () 
=> {
+    expect(shouldUseMultilineFormatting("first\nsecond")).toBe(true);
+    expect(shouldUseMultilineFormatting("first\\nsecond")).toBe(true);
+    expect(shouldUseMultilineFormatting("plain words")).toBe(false);
+  });
+
+  it("converts escaped newlines for display and leaves real newlines for 
save", () => {
+    expect(formatParamsForDisplay("first\\nsecond")).toBe("first\nsecond");
+    expect(formatParamsForSave("first\nsecond")).toBe("first\nsecond");
   });
 });
diff --git a/ambari-web/latest/src/Utils/jvmFormatUtils.ts 
b/ambari-web/latest/src/Utils/jvmFormatUtils.ts
index 2cfdb7d75c..dd1b999bb0 100644
--- a/ambari-web/latest/src/Utils/jvmFormatUtils.ts
+++ b/ambari-web/latest/src/Utils/jvmFormatUtils.ts
@@ -16,66 +16,62 @@
  * limitations under the License.
  */
 
-const splitQuotedParameters = (value: string): string[] => {
-  const parameters: string[] = [];
-  let current = "";
-  let quote = "";
-  let escaped = false;
-
-  for (const character of value) {
-    if (escaped) {
-      current += character;
-      escaped = false;
-      continue;
-    }
-    if (character === "\\") {
-      current += character;
-      escaped = true;
-      continue;
-    }
-    if (quote) {
-      current += character;
-      if (character === quote) quote = "";
-      continue;
-    }
-    if (character === '"' || character === "'") {
-      current += character;
-      quote = character;
-      continue;
-    }
-    if (/\s/.test(character)) {
-      if (current) parameters.push(current);
-      current = "";
-      continue;
-    }
-    current += character;
-  }
-  if (current) parameters.push(current);
-  return parameters;
+// Checks if a string contains only a single line. Also treats an escaped
+// newline sequence from the backend as a newline.
+const isSingleLine = (value: string): boolean => {
+  const stringValue = String(value).trim();
+  return stringValue.indexOf("\n") === -1 && stringValue.indexOf("\\n") === -1;
 };
 
-const startsLikeJvmOption = (value: string) =>
-  /^(?:-X|-D|-XX:|-server$|-client$|--add-(?:opens|exports)=)/.test(value);
-
-export const formatParamsForDisplay = (
-  value: string,
-  _displayType?: string,
-): string => splitQuotedParameters(String(value ?? "")).join("\n");
-
-export const formatParamsForSave = (value: string): string =>
-  splitQuotedParameters(String(value ?? "")).join(" ");
-
+// Ember's logic: an explicit displayType from the backend takes priority;
+// otherwise fall back to content-based detection. Values that merely look
+// like command-line/JVM arguments (e.g. mapreduce.admin.map.child.java.opts:
+// "-server -XX:NewRatio=8 -Djava.net.preferIPv4Stack=true 
-Dhdp.version=${hdp.version}")
+// must NOT be treated as multiline just because they contain multiple 
"-X"-style
+// tokens — only genuine (real or escaped) newlines should.
 export const shouldUseMultilineFormatting = (
   value: string,
   displayType?: string,
 ): boolean => {
-  if (
-    ["content", "directories", "directory", "multiLine"].includes(
-      String(displayType),
-    )
-  ) {
+  if (!value || typeof value !== "string") {
     return false;
   }
-  const parameters = splitQuotedParameters(String(value ?? ""));
-  return parameters.length > 1 && parameters.every(startsLikeJvmOption);
+  if (displayType) {
+    return displayType === "multiLine";
+  }
+  return !isSingleLine(value);
+};
+
+export const formatParamsForDisplay = (
+  value: string,
+  displayType?: string,
+): string => {
+  if (!value || typeof value !== "string") {
+    return value;
+  }
+  if (!shouldUseMultilineFormatting(value, displayType)) {
+    return value;
+  }
+  // Already has real newlines; let the textarea render it naturally.
+  if (value.includes("\n")) {
+    return value;
+  }
+  // Convert escaped newline sequences to actual newlines for display.
+  if (value.includes("\\n")) {
+    return value.replace(/\\n/g, "\n");
+  }
+  return value;
+};
+
+// Leaves real newlines as real newlines — JSON.stringify() will escape them
+// when the save payload is serialized. Escaping here too would double-escape
+// (\n -> \\n -> \\\\n).
+export const formatParamsForSave = (value: string): string => {
+  if (!value || typeof value !== "string") {
+    return value;
+  }
+  if (value.includes("\\n")) {
+    return value;
+  }
+  return value;
 };
diff --git a/ambari-web/latest/src/screens/ServiceConfigs/index.tsx 
b/ambari-web/latest/src/screens/ServiceConfigs/index.tsx
index e94e556b7b..c71236680a 100644
--- a/ambari-web/latest/src/screens/ServiceConfigs/index.tsx
+++ b/ambari-web/latest/src/screens/ServiceConfigs/index.tsx
@@ -231,7 +231,8 @@ export default function ServiceConfigs({
   async function onVersionChange(versionNumber: any) {
     const requestId = ++versionRequestId.current;
     try {
-      setSelectedVersion(versionNumber);
+      // Normalize to string so it compares correctly against 
defaultVersionNumber.
+      setSelectedVersion(String(versionNumber));
       let apiVersionNumber = versionNumber;
       if (configGroup !== "Default") {
         apiVersionNumber = defaultVersionNumber + "," + versionNumber;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to