From 6a0a00e4b7f9dd671e35bd10cdff245999a25959 Mon Sep 17 00:00:00 2001 From: GUAN MING Date: Sun, 24 Aug 2025 15:54:14 +0800 Subject: [PATCH 1/2] Allow preloaded options and params through search params --- .../HITLTaskInstances/HITLResponseForm.tsx | 18 ++++++---- airflow-core/src/airflow/ui/src/utils/hitl.ts | 33 +++++++++++++++++-- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx index b15f07aa1aa9f..a2d6a20e114bf 100644 --- a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx +++ b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx @@ -20,13 +20,14 @@ import { Button, Box, Spacer, HStack, Accordion, Text } from "@chakra-ui/react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { FiSend } from "react-icons/fi"; +import { useSearchParams } from "react-router-dom"; import type { HITLDetail, TaskInstanceResponse } from "openapi/requests/types.gen"; import { FlexibleForm } from "src/components/FlexibleForm/FlexibleForm"; import Time from "src/components/Time"; import { useParamStore } from "src/queries/useParamStore"; import { useUpdateHITLDetail } from "src/queries/useUpdateHITLDetail"; -import { getHITLParamsDict, getHITLFormData } from "src/utils/hitl"; +import { getHITLParamsDict, getHITLFormData, getPreloadHITLFormData } from "src/utils/hitl"; type HITLResponseFormProps = { readonly hitlDetail: { @@ -34,9 +35,12 @@ type HITLResponseFormProps = { } & Omit; }; -const isHighlightOption = (option: string, hitlDetail: HITLDetail) => { +const isHighlightOption = (option: string, hitlDetail: HITLDetail, preloadedHITLOptions: Array) => { + // preload's priority is higher than default + const defaultOptions = preloadedHITLOptions.length > 0 ? preloadedHITLOptions : hitlDetail.defaults; + const isSelected = hitlDetail.chosen_options?.includes(option) && Boolean(hitlDetail.response_received); - const isDefault = hitlDetail.defaults?.includes(option) && !Boolean(hitlDetail.response_received); + const isDefault = defaultOptions?.includes(option) && !Boolean(hitlDetail.response_received); // highlight if: // 1. the option is selected and the response is received @@ -50,6 +54,8 @@ export const HITLResponseForm = ({ hitlDetail }: HITLResponseFormProps) => { const [errors, setErrors] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const { paramsDict } = useParamStore("hitl"); + const [searchParams] = useSearchParams(); + const { preloadedHITLOptions } = getPreloadHITLFormData(searchParams); const { updateHITLResponse } = useUpdateHITLDetail({ dagId: hitlDetail.task_instance.dag_id, dagRunId: hitlDetail.task_instance.dag_run_id, @@ -97,7 +103,7 @@ export const HITLResponseForm = ({ hitlDetail }: HITLResponseFormProps) => { flexFormDescription={hitlDetail.body ?? undefined} flexibleFormDefaultSection={hitlDetail.subject} initialParamsDict={{ - paramsDict: getHITLParamsDict(hitlDetail, translate), + paramsDict: getHITLParamsDict(hitlDetail, translate, searchParams), }} isHITL key={hitlDetail.subject} @@ -112,11 +118,11 @@ export const HITLResponseForm = ({ hitlDetail }: HITLResponseFormProps) => { {hitlDetail.options.length < 4 && !hitlDetail.multiple ? ( hitlDetail.options.map((option) => ( diff --git a/airflow-core/src/airflow/ui/src/utils/hitl.ts b/airflow-core/src/airflow/ui/src/utils/hitl.ts index 8577ccd891107..a06754d6cbdba 100644 --- a/airflow-core/src/airflow/ui/src/utils/hitl.ts +++ b/airflow-core/src/airflow/ui/src/utils/hitl.ts @@ -33,10 +33,37 @@ const getChosenOptionsValue = (hitlDetail: HITLDetail) => { return hitlDetail.multiple ? sourceValues : sourceValues?.[0]; }; -export const getHITLParamsDict = (hitlDetail: HITLDetail, translate: TFunction): ParamsSpec => { +export const getPreloadHITLFormData = (searchParams: URLSearchParams) => { + const preloadedHITLParams: Record = Object.fromEntries( + [...searchParams.entries()] + .filter(([key]) => key !== "_options") + .map(([key, value]) => [key, isNaN(Number(value)) ? value : Number(value)]), + ); + + const options = searchParams.get("_options") ?? ""; + const preloadedHITLOptions: Array = options + ? options.split(",").filter((option) => option.length > 0) + : []; + + return { + preloadedHITLOptions, + preloadedHITLParams, + }; +}; + +export const getHITLParamsDict = ( + hitlDetail: HITLDetail, + translate: TFunction, + searchParams: URLSearchParams, +): ParamsSpec => { const paramsDict: ParamsSpec = {}; + const { preloadedHITLOptions, preloadedHITLParams } = getPreloadHITLFormData(searchParams); if (hitlDetail.options.length > 4 || hitlDetail.multiple) { + const filteredPreloadedHITLOptions = preloadedHITLOptions.filter((option) => + hitlDetail.options.includes(option), + ); + paramsDict.chosen_options = { description: translate("hitl:response.optionsDescription"), schema: { @@ -56,7 +83,7 @@ export const getHITLParamsDict = (hitlDetail: HITLDetail, translate: TFunction): values_display: undefined, }, - value: getChosenOptionsValue(hitlDetail), + value: getChosenOptionsValue(hitlDetail) ?? filteredPreloadedHITLOptions, }; } @@ -84,7 +111,7 @@ export const getHITLParamsDict = (hitlDetail: HITLDetail, translate: TFunction): type: valueType, values_display: undefined, }, - value, + value: preloadedHITLParams[key] ?? value, }; }); } From a67e8d2f150bf340b3ed9d0eb60cfdb6d32577bd Mon Sep 17 00:00:00 2001 From: "Guan Ming(Wesley) Chiu" <105915352+guan404ming@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:43:55 +0800 Subject: [PATCH 2/2] Update render logic --- .../HITLTaskInstances/HITLResponseForm.tsx | 13 +++++- airflow-core/src/airflow/ui/src/utils/hitl.ts | 43 +++++++++++++------ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx index a2d6a20e114bf..ec87c414025d4 100644 --- a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx +++ b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx @@ -55,7 +55,16 @@ export const HITLResponseForm = ({ hitlDetail }: HITLResponseFormProps) => { const [isSubmitting, setIsSubmitting] = useState(false); const { paramsDict } = useParamStore("hitl"); const [searchParams] = useSearchParams(); - const { preloadedHITLOptions } = getPreloadHITLFormData(searchParams); + const { preloadedHITLOptions } = getPreloadHITLFormData(searchParams, hitlDetail); + + const isApprovalTask = + hitlDetail.options.includes("Approve") && + hitlDetail.options.includes("Reject") && + hitlDetail.options.length === 2; + + const shouldRenderOptionButton = + hitlDetail.options.length < 4 && !hitlDetail.multiple && preloadedHITLOptions.length === 0; + const { updateHITLResponse } = useUpdateHITLDetail({ dagId: hitlDetail.task_instance.dag_id, dagRunId: hitlDetail.task_instance.dag_run_id, @@ -115,7 +124,7 @@ export const HITLResponseForm = ({ hitlDetail }: HITLResponseFormProps) => { - {hitlDetail.options.length < 4 && !hitlDetail.multiple ? ( + {shouldRenderOptionButton || isApprovalTask ? ( hitlDetail.options.map((option) => (