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..ec87c414025d4 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,17 @@ 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, 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, @@ -97,7 +112,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} @@ -109,14 +124,14 @@ export const HITLResponseForm = ({ hitlDetail }: HITLResponseFormProps) => { - {hitlDetail.options.length < 4 && !hitlDetail.multiple ? ( + {shouldRenderOptionButton || isApprovalTask ? ( 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..0ac1386d301f2 100644 --- a/airflow-core/src/airflow/ui/src/utils/hitl.ts +++ b/airflow-core/src/airflow/ui/src/utils/hitl.ts @@ -33,10 +33,51 @@ const getChosenOptionsValue = (hitlDetail: HITLDetail) => { return hitlDetail.multiple ? sourceValues : sourceValues?.[0]; }; -export const getHITLParamsDict = (hitlDetail: HITLDetail, translate: TFunction): ParamsSpec => { - const paramsDict: ParamsSpec = {}; +export const getPreloadHITLFormData = (searchParams: URLSearchParams, hitlDetail: HITLDetail) => { + 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") ?? ""; + let preloadedHITLOptions: Array = []; + + if (options) { + try { + const decoded = JSON.parse(decodeURIComponent(options)) as Array; + + preloadedHITLOptions = Array.isArray(decoded) ? decoded : []; + } catch { + preloadedHITLOptions = []; + } + } - if (hitlDetail.options.length > 4 || hitlDetail.multiple) { + // Filter the preloaded options to only include the options that are in the hitlDetail.options + const filteredPreloadedHITLOptions: Array | string | undefined = preloadedHITLOptions.filter( + (option) => hitlDetail.options.includes(option), + ); + + return { + preloadedHITLOptions: filteredPreloadedHITLOptions, + preloadedHITLParams, + }; +}; + +export const getHITLParamsDict = ( + hitlDetail: HITLDetail, + translate: TFunction, + searchParams: URLSearchParams, +): ParamsSpec => { + const paramsDict: ParamsSpec = {}; + const { preloadedHITLOptions, preloadedHITLParams } = getPreloadHITLFormData(searchParams, hitlDetail); + const isApprovalTask = + hitlDetail.options.includes("Approve") && + hitlDetail.options.includes("Reject") && + hitlDetail.options.length === 2; + const shouldRenderOptionDropdown = preloadedHITLOptions.length > 0 && !isApprovalTask; + + if (shouldRenderOptionDropdown || hitlDetail.options.length > 4 || hitlDetail.multiple) { paramsDict.chosen_options = { description: translate("hitl:response.optionsDescription"), schema: { @@ -56,7 +97,10 @@ export const getHITLParamsDict = (hitlDetail: HITLDetail, translate: TFunction): values_display: undefined, }, - value: getChosenOptionsValue(hitlDetail), + // If the task is not multiple, we only show the first option + value: + getChosenOptionsValue(hitlDetail) ?? + (hitlDetail.multiple ? preloadedHITLOptions : preloadedHITLOptions[0]), }; } @@ -84,7 +128,7 @@ export const getHITLParamsDict = (hitlDetail: HITLDetail, translate: TFunction): type: valueType, values_display: undefined, }, - value, + value: preloadedHITLParams[key] ?? value, }; }); }