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}