This is an automated email from the ASF dual-hosted git repository.
bbovenzi 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 caab2a99d11 Fix for object rendering in HITL interface (#58611)
caab2a99d11 is described below
commit caab2a99d11ed563b990fec43071d724d69b1c27
Author: Stuart Buckingham <[email protected]>
AuthorDate: Tue Nov 25 12:17:15 2025 -0800
Fix for object rendering in HITL interface (#58611)
---
airflow-core/src/airflow/ui/src/utils/hitl.test.ts | 65 ++++++++++++++++++++++
airflow-core/src/airflow/ui/src/utils/hitl.ts | 33 ++++++++++-
2 files changed, 95 insertions(+), 3 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/utils/hitl.test.ts
b/airflow-core/src/airflow/ui/src/utils/hitl.test.ts
new file mode 100644
index 00000000000..daead151359
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/utils/hitl.test.ts
@@ -0,0 +1,65 @@
+/*!
+ * 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 type { TFunction } from "i18next";
+import { describe, it, expect, vi } from "vitest";
+
+import type { HITLDetail } from "openapi/requests/types.gen";
+
+import { getHITLParamsDict } from "./hitl";
+
+const mockTranslate = vi.fn((key: string) => key) as unknown as TFunction;
+
+const createMockHITLDetail = (overrides?: Partial<HITLDetail>): HITLDetail =>
+ ({
+ assigned_users: [],
+ body: "Test Body",
+ chosen_options: [],
+ created_at: new Date().toISOString(),
+ defaults: ["Option1"],
+ multiple: false,
+ options: ["Option1", "Option2"],
+ params: {},
+ params_input: {},
+ response_received: false,
+ subject: "Test Subject",
+ task_instance: {
+ dag_id: "test_dag",
+ dag_run_id: "test_run",
+ map_index: -1,
+ state: "deferred",
+ task_id: "test_task",
+ },
+ ...overrides,
+ }) as HITLDetail;
+
+describe("getHITLParamsDict", () => {
+ it("correctly types object parameters as 'object' instead of 'string'", ()
=> {
+ const hitlDetail = createMockHITLDetail({
+ params: {
+ objectParam: { key: "value", nested: { data: 123 } },
+ },
+ });
+
+ const searchParams = new URLSearchParams();
+ const paramsDict = getHITLParamsDict(hitlDetail, mockTranslate,
searchParams);
+
+ expect(paramsDict.objectParam?.schema.type).toBe("object");
+ expect(paramsDict.objectParam?.value).toEqual({ key: "value", nested: {
data: 123 } });
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/utils/hitl.ts
b/airflow-core/src/airflow/ui/src/utils/hitl.ts
index bd3a4051754..6fb7745ded3 100644
--- a/airflow-core/src/airflow/ui/src/utils/hitl.ts
+++ b/airflow-core/src/airflow/ui/src/utils/hitl.ts
@@ -70,7 +70,7 @@ export const getHITLParamsDict = (
searchParams: URLSearchParams,
): ParamsSpec => {
const paramsDict: ParamsSpec = {};
- const { preloadedHITLOptions } = getPreloadHITLFormData(searchParams,
hitlDetail);
+ const { preloadedHITLOptions, preloadedHITLParams } =
getPreloadHITLFormData(searchParams, hitlDetail);
const isApprovalTask =
hitlDetail.options.includes("Approve") &&
hitlDetail.options.includes("Reject") &&
@@ -113,9 +113,36 @@ export const getHITLParamsDict = (
}
const paramData = hitlDetail.params[key] as ParamsSpec | undefined;
+ // Check if there's a preloaded value from URL params
+ let finalValue = preloadedHITLParams[key] ?? value;
+
+ // If preloaded value is a string that might be JSON, try to parse it
+ if (typeof finalValue === "string" && finalValue.trim().startsWith("{"))
{
+ try {
+ const parsed: unknown = JSON.parse(finalValue);
+
+ if (typeof parsed === "object" && parsed !== null &&
!Array.isArray(parsed)) {
+ finalValue = parsed;
+ }
+ } catch {
+ // If parsing fails, keep the string value
+ }
+ }
+
const description: string =
paramData && typeof paramData.description === "string" ?
paramData.description : "";
+ // Determine the type based on the final value
+ let valueType: string;
+
+ if (typeof finalValue === "number") {
+ valueType = "number";
+ } else if (typeof finalValue === "object" && finalValue !== null &&
!Array.isArray(finalValue)) {
+ valueType = "object";
+ } else {
+ valueType = "string";
+ }
+
const schema: ParamSchema = {
const: undefined,
description_md: "",
@@ -129,7 +156,7 @@ export const getHITLParamsDict = (
minLength: undefined,
section: undefined,
title: key,
- type: typeof value === "number" ? "number" : "string",
+ type: valueType,
values_display: undefined,
...(paramData?.schema && typeof paramData.schema === "object" ?
paramData.schema : {}),
};
@@ -137,7 +164,7 @@ export const getHITLParamsDict = (
paramsDict[key] = {
description,
schema,
- value: paramData?.value ?? value,
+ value: paramData?.value ?? finalValue,
};
});
}