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

potiuk pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new 7c66d3b95be [v3-3-test] UI: Move JSON validation and prettifying into 
JsonEditor (#70380) (#70554)
7c66d3b95be is described below

commit 7c66d3b95be89804efc7ff68f715abab4d18e0bb
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Jul 28 19:10:04 2026 +0200

    [v3-3-test] UI: Move JSON validation and prettifying into JsonEditor 
(#70380) (#70554)
    
    The validate-and-prettify logic for JSON input lived in
    CreateAssetEventModal with a TODO asking to move it into the shared
    JsonEditor. It now lives behind new optional prettify/onError props so
    any JSON input in the UI can opt in instead of re-implementing it.
    (cherry picked from commit f5e3c1ef26db950cb673205df736e1e8fb77c43e)
    
    Co-authored-by: Yang <[email protected]>
---
 .../airflow/ui/src/components/JsonEditor.test.tsx  | 86 ++++++++++++++++++++++
 .../src/airflow/ui/src/components/JsonEditor.tsx   | 23 +++++-
 .../ui/src/pages/Asset/CreateAssetEventModal.tsx   | 21 +-----
 3 files changed, 108 insertions(+), 22 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/JsonEditor.test.tsx 
b/airflow-core/src/airflow/ui/src/components/JsonEditor.test.tsx
new file mode 100644
index 00000000000..555bf6ad42a
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/JsonEditor.test.tsx
@@ -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 "@testing-library/jest-dom";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { JsonEditor } from "./JsonEditor";
+
+vi.mock("src/components/MonacoEditor", () => ({
+  default: ({
+    onChange,
+    value,
+  }: {
+    readonly onChange?: (value: string | undefined) => void;
+    readonly value?: string;
+  }) => (
+    <textarea aria-label="JSON editor" onChange={(event) => 
onChange?.(event.target.value)} value={value} />
+  ),
+}));
+
+vi.mock("src/context/colorMode", () => ({
+  useMonacoTheme: () => ({ beforeMount: vi.fn(), theme: "airflow-light" }),
+}));
+
+describe("JsonEditor", () => {
+  it("passes the raw value through when prettify is off", () => {
+    const onChange = vi.fn();
+
+    render(<JsonEditor onChange={onChange} value="{}" />);
+
+    fireEvent.change(screen.getByLabelText("JSON editor"), { target: { value: 
'{"key":1}' } });
+
+    expect(onChange).toHaveBeenCalledWith('{"key":1}');
+  });
+
+  it("prettifies valid JSON and clears the error when prettify is on", () => {
+    const onChange = vi.fn();
+    const onError = vi.fn();
+
+    render(<JsonEditor onChange={onChange} onError={onError} prettify 
value="{}" />);
+
+    fireEvent.change(screen.getByLabelText("JSON editor"), { target: { value: 
'{"key":1}' } });
+
+    expect(onError).toHaveBeenCalledWith(undefined);
+    expect(onChange).toHaveBeenCalledWith(JSON.stringify({ key: 1 }, 
undefined, 2));
+  });
+
+  it("does not call onChange when the prettified JSON is unchanged", () => {
+    const onChange = vi.fn();
+    const formatted = JSON.stringify({ key: 1 }, undefined, 2);
+
+    render(<JsonEditor onChange={onChange} prettify value={formatted} />);
+
+    fireEvent.change(screen.getByLabelText("JSON editor"), { target: { value: 
'{"key": 1}' } });
+
+    expect(onChange).not.toHaveBeenCalled();
+  });
+
+  it("reports a parse error and skips onChange for invalid JSON when prettify 
is on", () => {
+    const onChange = vi.fn();
+    const onError = vi.fn();
+
+    render(<JsonEditor onChange={onChange} onError={onError} prettify 
value="{}" />);
+
+    fireEvent.change(screen.getByLabelText("JSON editor"), { target: { value: 
"{invalid" } });
+
+    expect(onError).toHaveBeenCalledWith(expect.any(String));
+    expect(onChange).not.toHaveBeenCalled();
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/components/JsonEditor.tsx 
b/airflow-core/src/airflow/ui/src/components/JsonEditor.tsx
index 060df05ebce..a8ffb954cb8 100644
--- a/airflow-core/src/airflow/ui/src/components/JsonEditor.tsx
+++ b/airflow-core/src/airflow/ui/src/components/JsonEditor.tsx
@@ -28,6 +28,8 @@ type JsonEditorProps = {
   readonly name?: string;
   readonly onBlur?: () => void;
   readonly onChange?: (value: string) => void;
+  readonly onError?: (error: string | undefined) => void;
+  readonly prettify?: boolean;
   readonly value?: string;
 };
 
@@ -36,6 +38,8 @@ export const JsonEditor = ({
   height = "200px",
   onBlur,
   onChange,
+  onError,
+  prettify = false,
   value,
   ...rest
 }: JsonEditorProps) => {
@@ -55,8 +59,23 @@ export const JsonEditor = ({
     scrollBeyondLastLine: false,
   };
 
-  const handleChange = (val: string | undefined) => {
-    onChange?.(val ?? "");
+  const handleChange = (val: string | undefined = "") => {
+    if (!prettify) {
+      onChange?.(val);
+
+      return;
+    }
+
+    try {
+      const formattedJson = JSON.stringify(JSON.parse(val) as unknown, 
undefined, 2);
+
+      onError?.(undefined);
+      if (formattedJson !== value) {
+        onChange?.(formattedJson);
+      }
+    } catch (error) {
+      onError?.(error instanceof Error ? error.message : String(error));
+    }
   };
 
   return (
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx 
b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx
index c952b45b30c..b4e64b0f1d8 100644
--- a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx
@@ -72,25 +72,6 @@ export const CreateAssetEventModal = ({ asset, onClose, open 
}: Props) => {
   const [upstreamDag] = upstreamDags;
   const upstreamDagId = hasUpstreamDag ? 
upstreamDag?.source_id.replace("dag:", "") : undefined;
 
-  // TODO move validate + prettify into JsonEditor
-  const validateAndPrettifyJson = (newValue: string) => {
-    try {
-      const parsedJson = JSON.parse(newValue) as JSON;
-
-      setExtraError(undefined);
-
-      const formattedJson = JSON.stringify(parsedJson, undefined, 2);
-
-      if (formattedJson !== extra) {
-        setExtra(formattedJson); // Update only if the value is different
-      }
-    } catch (error) {
-      const errorMessage = error instanceof Error ? error.message : 
translate("common:error.unknown");
-
-      setExtraError(errorMessage);
-    }
-  };
-
   const onSuccess = async (response: AssetEventResponse | DAGRunResponse) => {
     setExtra("{}");
     setExtraError(undefined);
@@ -219,7 +200,7 @@ export const CreateAssetEventModal = ({ asset, onClose, 
open }: Props) => {
           {eventType === "manual" ? (
             <Field.Root mt={6}>
               <Field.Label 
fontSize="md">{translate("createEvent.manual.extra")}</Field.Label>
-              <JsonEditor onChange={validateAndPrettifyJson} value={extra} />
+              <JsonEditor onChange={setExtra} onError={setExtraError} prettify 
value={extra} />
               <Text color="fg.error">{extraError}</Text>
             </Field.Root>
           ) : undefined}

Reply via email to