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>