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

pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 5b77a71b344 Better warning visibility for invalid JSON value when 
editing variables (#68268)
5b77a71b344 is described below

commit 5b77a71b344b1ef8c18dd41bd69171fc534b00de
Author: Christ Beaubrun <[email protected]>
AuthorDate: Mon Jun 22 11:10:44 2026 -0500

    Better warning visibility for invalid JSON value when editing variables 
(#68268)
    
    * Fix variable form invalid JSON validation
    
    * Make invalid JSON warning non-blocking
    
    * Update variable JSON warning tests
    
    * refactor(ui): use shared Alert for variable JSON warnings
    
    * test: satisfy ui lint for variable JSON warning
---
 .../Variables/ManageVariable/VariableForm.test.tsx | 123 +++++++++++++++++++++
 .../Variables/ManageVariable/VariableForm.tsx      |   7 +-
 2 files changed, 127 insertions(+), 3 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/VariableForm.test.tsx
 
b/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/VariableForm.test.tsx
new file mode 100644
index 00000000000..70d45765cb9
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/VariableForm.test.tsx
@@ -0,0 +1,123 @@
+/*!
+ * 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 { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { Wrapper } from "src/utils/Wrapper";
+
+import VariableForm, { type VariableBody } from "./VariableForm";
+
+vi.mock("src/queries/useConfig.tsx", () => ({
+  useConfig: () => false,
+}));
+
+const initialVariable: VariableBody = {
+  description: "",
+  key: "json_config",
+  team_name: "",
+  value: '{"enabled": true}',
+};
+
+const renderVariableForm = (manageMutate = vi.fn()) => {
+  render(
+    <VariableForm
+      error={undefined}
+      initialVariable={initialVariable}
+      isPending={false}
+      manageMutate={manageMutate}
+      setError={vi.fn()}
+    />,
+    { wrapper: Wrapper },
+  );
+
+  return manageMutate;
+};
+
+describe("VariableForm", () => {
+  it("shows a prominent warning for malformed JSON object values without 
blocking save", async () => {
+    const manageMutate = renderVariableForm();
+
+    fireEvent.change(screen.getByLabelText(/value/iu), { target: { value: 
'{"enabled": true,' } });
+
+    await waitFor(() => 
expect(screen.getByText("variables.form.invalidJson")).toBeInTheDocument());
+    expect(screen.getByRole("button", { name: /save/iu })).toBeEnabled();
+
+    fireEvent.click(screen.getByRole("button", { name: /save/iu }));
+
+    await waitFor(() =>
+      expect(manageMutate).toHaveBeenCalledWith({
+        ...initialVariable,
+        value: '{"enabled": true,',
+      }),
+    );
+  });
+
+  it("allows saving non-JSON string values that start with a bracket", async 
() => {
+    const manageMutate = renderVariableForm();
+
+    fireEvent.change(screen.getByLabelText(/value/iu), { target: { value: 
"[DRAFT] plain string value" } });
+
+    await waitFor(() => 
expect(screen.getByText("variables.form.invalidJson")).toBeInTheDocument());
+    expect(screen.getByRole("button", { name: /save/iu })).toBeEnabled();
+
+    fireEvent.click(screen.getByRole("button", { name: /save/iu }));
+
+    await waitFor(() =>
+      expect(manageMutate).toHaveBeenCalledWith({
+        ...initialVariable,
+        value: "[DRAFT] plain string value",
+      }),
+    );
+  });
+
+  it("allows saving Jinja template values", async () => {
+    const manageMutate = renderVariableForm();
+
+    fireEvent.change(screen.getByLabelText(/value/iu), { target: { value: "{{ 
var.value.x }}" } });
+
+    await waitFor(() => 
expect(screen.getByText("variables.form.invalidJson")).toBeInTheDocument());
+    expect(screen.getByRole("button", { name: /save/iu })).toBeEnabled();
+
+    fireEvent.click(screen.getByRole("button", { name: /save/iu }));
+
+    await waitFor(() =>
+      expect(manageMutate).toHaveBeenCalledWith({
+        ...initialVariable,
+        value: "{{ var.value.x }}",
+      }),
+    );
+  });
+
+  it("allows saving plain string values", async () => {
+    const manageMutate = renderVariableForm();
+
+    fireEvent.change(screen.getByLabelText(/value/iu), { target: { value: 
"plain string value" } });
+
+    await waitFor(() => expect(screen.getByRole("button", { name: /save/iu 
})).toBeEnabled());
+
+    fireEvent.click(screen.getByRole("button", { name: /save/iu }));
+
+    await waitFor(() =>
+      expect(manageMutate).toHaveBeenCalledWith({
+        ...initialVariable,
+        value: "plain string value",
+      }),
+    );
+  });
+});
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/VariableForm.tsx
 
b/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/VariableForm.tsx
index dd9f95245c6..d555f641a99 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/VariableForm.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/VariableForm.tsx
@@ -16,13 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Button, Field, HStack, Input, Spacer, Text, Textarea } from 
"@chakra-ui/react";
+import { Box, Button, Field, HStack, Input, Spacer, Textarea } from 
"@chakra-ui/react";
 import { Controller, useForm } from "react-hook-form";
 import { useTranslation } from "react-i18next";
 import { FiSave } from "react-icons/fi";
 
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { TeamSelector } from "src/components/TeamSelector.tsx";
+import { Alert } from "src/components/ui";
 import { useConfig } from "src/queries/useConfig.tsx";
 
 export type VariableBody = {
@@ -106,9 +107,9 @@ const VariableForm = ({ error, initialVariable, isPending, 
manageMutate, setErro
               </Field.Label>
               <Textarea {...field} size="sm" />
               {showJsonWarning ? (
-                <Text color="fg.warning" fontSize="xs">
+                <Alert mt={2} status="warning">
                   {translate("variables.form.invalidJson")}
-                </Text>
+                </Alert>
               ) : undefined}
               {fieldState.error ? 
<Field.ErrorText>{fieldState.error.message}</Field.ErrorText> : undefined}
             </Field.Root>

Reply via email to