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

jasonliu 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 7524107bf5c feature:Trigger form missing "Select Recent 
Configurations" from airflow 2 (#56406)
7524107bf5c is described below

commit 7524107bf5ca6254abf43e024c33c1fb5e1dd7c6
Author: KUAN-HAO HUANG <[email protected]>
AuthorDate: Fri Jan 16 14:57:01 2026 +0800

    feature:Trigger form missing "Select Recent Configurations" from airflow 2 
(#56406)
    
    * add a recent configrations drop down list
    
    * remove extra files
    
    * refactor: change trigger config UX from dropdown to menu button
    
    - Remove RecentConfigurationsDropdown component
    - Add menu option 'Trigger again with this config' to Trigger button
    - Pre-fill form with selected DAG Run configuration
    - Revert unnecessary API changes for recent configurations
    - Update translations for new menu option
    
    * follow suggestions for 409 conflict
    
    * follow suggestions and pick up types
    
    * fix the staticcheck error
---
 .../ui/public/i18n/locales/en/components.json      |  1 +
 .../ui/public/i18n/locales/zh-CN/components.json   |  1 +
 .../src/components/DagActions/RunBackfillForm.tsx  |  2 +-
 .../TriggerDag/TriggerDAGAdvancedOptions.tsx       |  2 +-
 .../src/components/TriggerDag/TriggerDAGButton.tsx | 89 +++++++++++++++++++++-
 .../src/components/TriggerDag/TriggerDAGForm.tsx   | 73 ++++++++++++------
 .../src/components/TriggerDag/TriggerDAGModal.tsx  |  9 +++
 .../airflow/ui/src/components/TriggerDag/types.ts  | 36 +++++++++
 .../src/airflow/ui/src/queries/useParamStore.ts    | 42 +++++++---
 .../src/airflow/ui/src/queries/useTrigger.ts       |  2 +-
 10 files changed, 220 insertions(+), 37 deletions(-)

diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json
index 8b1cacd62ce..f8f3b666c7e 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json
@@ -143,6 +143,7 @@
         "title": "Dag Run Triggered"
       }
     },
+    "triggerAgainWithConfig": "Trigger again with this config",
     "unpause": "Unpause {{dagDisplayName}} on trigger"
   },
   "trimText": {
diff --git 
a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json
index b52f071f607..13d6c72767c 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json
@@ -115,6 +115,7 @@
         "title": "已触发 Dag 执行"
       }
     },
+    "triggerAgainWithConfig": "使用此配置再次触发",
     "unpause": "触发时取消暂停 {{dagDisplayName}}"
   },
   "trimText": {
diff --git 
a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx 
b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
index 664b9902f4e..089c914707d 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
@@ -34,7 +34,7 @@ import { useTogglePause } from "src/queries/useTogglePause";
 import ConfigForm from "../ConfigForm";
 import { DateTimeInput } from "../DateTimeInput";
 import { ErrorAlert, type ExpandedApiError } from "../ErrorAlert";
-import type { DagRunTriggerParams } from "../TriggerDag/TriggerDAGForm";
+import type { DagRunTriggerParams } from "../TriggerDag/types";
 import { Alert } from "../ui";
 import { Checkbox } from "../ui/Checkbox";
 import { getInlineMessage } from "./inlineMessage";
diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx
 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx
index b8d3fe2b9f4..77d16664e52 100644
--- 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx
@@ -21,7 +21,7 @@ import { Controller, type Control } from "react-hook-form";
 import { useTranslation } from "react-i18next";
 
 import EditableMarkdown from "./EditableMarkdown";
-import type { DagRunTriggerParams } from "./TriggerDAGForm";
+import type { DagRunTriggerParams } from "./types";
 
 type TriggerDAGAdvancedOptionsProps = {
   readonly control: Control<DagRunTriggerParams>;
diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx
index ec8f8f24784..2756d7983c8 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx
@@ -18,8 +18,13 @@
  */
 import { Box } from "@chakra-ui/react";
 import { useDisclosure } from "@chakra-ui/react";
+import React from "react";
 import { useTranslation } from "react-i18next";
 import { FiPlay } from "react-icons/fi";
+import { useParams } from "react-router-dom";
+
+import { useDagRunServiceGetDagRun } from "openapi/queries";
+import { Menu } from "src/components/ui";
 
 import ActionButton from "../ui/ActionButton";
 import TriggerDAGModal from "./TriggerDAGModal";
@@ -34,13 +39,92 @@ type Props = {
 const TriggerDAGButton: React.FC<Props> = ({ dagDisplayName, dagId, isPaused, 
withText = true }) => {
   const { onClose, onOpen, open } = useDisclosure();
   const { t: translate } = useTranslation("components");
+  const { runId } = useParams();
+  const [prefillConfig, setPrefillConfig] = React.useState<
+    | {
+        conf: Record<string, unknown> | undefined;
+        logicalDate: string | undefined;
+        runId: string;
+      }
+    | undefined
+  >(undefined);
+
+  // Check if there's a selected DAG Run
+  const { data: selectedDagRun } = useDagRunServiceGetDagRun(
+    {
+      dagId,
+      dagRunId: runId ?? "",
+    },
+    undefined,
+    { enabled: Boolean(dagId) && Boolean(runId) },
+  );
+
+  const handleTriggerWithConfig = () => {
+    if (selectedDagRun) {
+      setPrefillConfig({
+        conf: selectedDagRun.conf ?? undefined,
+        logicalDate: selectedDagRun.logical_date ?? undefined,
+        runId: selectedDagRun.dag_run_id,
+      });
+      onOpen();
+    }
+  };
+
+  const handleNormalTrigger = () => {
+    setPrefillConfig(undefined);
+    onOpen();
+  };
+
+  const handleModalClose = () => {
+    setPrefillConfig(undefined);
+    onClose();
+  };
+
+  // If there's a selected DAG Run with config, show menu with options
+  if (selectedDagRun?.conf !== undefined) {
+    return (
+      <Box>
+        <Menu.Root>
+          <Menu.Trigger asChild>
+            <div>
+              <ActionButton
+                actionName={translate("triggerDag.title")}
+                icon={<FiPlay />}
+                text={translate("triggerDag.button")}
+                variant="outline"
+                withText={withText}
+              />
+            </div>
+          </Menu.Trigger>
+          <Menu.Content>
+            <Menu.Item onClick={handleNormalTrigger} value="trigger">
+              {translate("triggerDag.button")}
+            </Menu.Item>
+            <Menu.Item onClick={handleTriggerWithConfig} 
value="triggerWithConfig">
+              {translate("triggerDag.triggerAgainWithConfig")}
+            </Menu.Item>
+          </Menu.Content>
+        </Menu.Root>
+
+        <TriggerDAGModal
+          dagDisplayName={dagDisplayName}
+          dagId={dagId}
+          isPaused={isPaused}
+          onClose={handleModalClose}
+          open={open}
+          prefillConfig={prefillConfig}
+        />
+      </Box>
+    );
+  }
 
+  // Normal trigger button without menu
   return (
     <Box>
       <ActionButton
         actionName={translate("triggerDag.title")}
         icon={<FiPlay />}
-        onClick={onOpen}
+        onClick={handleNormalTrigger}
         text={translate("triggerDag.button")}
         variant="outline"
         withText={withText}
@@ -50,8 +134,9 @@ const TriggerDAGButton: React.FC<Props> = ({ dagDisplayName, 
dagId, isPaused, wi
         dagDisplayName={dagDisplayName}
         dagId={dagId}
         isPaused={isPaused}
-        onClose={onClose}
+        onClose={handleModalClose}
         open={open}
+        prefillConfig={undefined}
       />
     </Box>
   );
diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
index cd291acb09e..f640d84aa60 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
@@ -35,6 +35,8 @@ import { ErrorAlert } from "../ErrorAlert";
 import { Checkbox } from "../ui/Checkbox";
 import { RadioCardItem, RadioCardRoot } from "../ui/RadioCard";
 import TriggerDAGAdvancedOptions from "./TriggerDAGAdvancedOptions";
+import type { DagRunTriggerParams } from "./types";
+import { dataIntervalModeOptions } from "./types";
 
 type TriggerDAGFormProps = {
   readonly dagDisplayName: string;
@@ -43,26 +45,15 @@ type TriggerDAGFormProps = {
   readonly isPaused: boolean;
   readonly onClose: () => void;
   readonly open: boolean;
+  readonly prefillConfig?:
+    | {
+        conf: Record<string, unknown> | undefined;
+        logicalDate: string | undefined;
+        runId: string;
+      }
+    | undefined;
 };
 
-type DataIntervalMode = "auto" | "manual";
-
-export type DagRunTriggerParams = {
-  conf: string;
-  dagRunId: string;
-  dataIntervalEnd: string;
-  dataIntervalMode: DataIntervalMode;
-  dataIntervalStart: string;
-  logicalDate: string;
-  note: string;
-  partitionKey: string | undefined;
-};
-
-const dataIntervalModeOptions: Array<{ label: string; value: DataIntervalMode 
}> = [
-  { label: "components:triggerDag.dataIntervalAuto", value: "auto" },
-  { label: "components:triggerDag.dataIntervalManual", value: "manual" },
-];
-
 const TriggerDAGForm = ({
   dagDisplayName,
   dagId,
@@ -70,13 +61,14 @@ const TriggerDAGForm = ({
   isPaused,
   onClose,
   open,
+  prefillConfig,
 }: TriggerDAGFormProps) => {
   const { t: translate } = useTranslation(["common", "components"]);
   const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
   const [formError, setFormError] = useState(false);
   const initialParamsDict = useDagParams(dagId, open);
   const { error: errorTrigger, isPending, triggerDagRun } = useTrigger({ 
dagId, onSuccessConfirm: onClose });
-  const { conf } = useParamStore();
+  const { conf, initialParamDict, setConf, setInitialParamDict } = 
useParamStore();
   const [unpause, setUnpause] = useState(true);
 
   const { mutate: togglePause } = useTogglePause({ dagId });
@@ -95,15 +87,51 @@ const TriggerDAGForm = ({
     },
   });
 
-  // Automatically reset form when conf is fetched
+  // Pre-fill form when prefillConfig is provided (priority over conf)
+  // Only restore 'conf' (parameters), not logicalDate, runId, or partitionKey 
to avoid 409 conflicts
   useEffect(() => {
-    if (conf) {
+    if (prefillConfig && open) {
+      const confString = prefillConfig.conf ? 
JSON.stringify(prefillConfig.conf, undefined, 2) : "";
+
+      reset({
+        conf: confString,
+        dagRunId: "",
+        dataIntervalEnd: "",
+        dataIntervalMode: "auto",
+        dataIntervalStart: "",
+        logicalDate: dayjs().format(DEFAULT_DATETIME_FORMAT),
+        note: "",
+        partitionKey: undefined,
+      });
+
+      // Also update the param store to keep it in sync.
+      // Wait until we have the initial params so section ordering stays 
consistent.
+      if (confString && Object.keys(initialParamsDict.paramsDict).length > 0) {
+        if (Object.keys(initialParamDict).length === 0) {
+          setInitialParamDict(initialParamsDict.paramsDict);
+        }
+        setConf(confString);
+      }
+    }
+  }, [
+    prefillConfig,
+    open,
+    reset,
+    setConf,
+    initialParamsDict.paramsDict,
+    initialParamDict,
+    setInitialParamDict,
+  ]);
+
+  // Automatically reset form when conf is fetched (only if no prefillConfig)
+  useEffect(() => {
+    if (conf && !prefillConfig && open) {
       reset((prevValues) => ({
         ...prevValues,
         conf,
       }));
     }
-  }, [conf, reset]);
+  }, [conf, prefillConfig, open, reset]);
 
   const resetDateError = () => {
     setErrors((prev) => ({ ...prev, date: undefined }));
@@ -116,7 +144,6 @@ const TriggerDAGForm = ({
   const dataIntervalInvalid =
     dataIntervalMode === "manual" &&
     (noDataInterval || 
dayjs(dataIntervalStart).isAfter(dayjs(dataIntervalEnd)));
-
   const onSubmit = (data: DagRunTriggerParams) => {
     if (unpause && isPaused) {
       togglePause({
diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx
index 9eb3e00a0e1..0562219a6ae 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx
@@ -38,6 +38,13 @@ type TriggerDAGModalProps = {
   readonly isPaused: boolean;
   readonly onClose: () => void;
   readonly open: boolean;
+  readonly prefillConfig?:
+    | {
+        conf: Record<string, unknown> | undefined;
+        logicalDate: string | undefined;
+        runId: string;
+      }
+    | undefined;
 };
 
 const TriggerDAGModal: React.FC<TriggerDAGModalProps> = ({
@@ -46,6 +53,7 @@ const TriggerDAGModal: React.FC<TriggerDAGModalProps> = ({
   isPaused,
   onClose,
   open,
+  prefillConfig,
 }) => {
   const { t: translate } = useTranslation("components");
   const [runMode, setRunMode] = useState<RunMode>(RunMode.SINGLE);
@@ -129,6 +137,7 @@ const TriggerDAGModal: React.FC<TriggerDAGModalProps> = ({
                   isPaused={isPaused}
                   onClose={onClose}
                   open={open}
+                  prefillConfig={prefillConfig}
                 />
               ) : (
                 hasSchedule && dag && <RunBackfillForm dag={dag} 
onClose={onClose} />
diff --git a/airflow-core/src/airflow/ui/src/components/TriggerDag/types.ts 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/types.ts
new file mode 100644
index 00000000000..53897dd6d33
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/types.ts
@@ -0,0 +1,36 @@
+/*!
+ * 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.
+ */
+
+export type DataIntervalMode = "auto" | "manual";
+
+export type DagRunTriggerParams = {
+  conf: string;
+  dagRunId: string;
+  dataIntervalEnd: string;
+  dataIntervalMode: DataIntervalMode;
+  dataIntervalStart: string;
+  logicalDate: string;
+  note: string;
+  partitionKey: string | undefined;
+};
+
+export const dataIntervalModeOptions = [
+  { label: "components:triggerDag.dataIntervalAuto", value: "auto" },
+  { label: "components:triggerDag.dataIntervalManual", value: "manual" },
+] as const satisfies Array<{ label: string; value: DataIntervalMode }>;
diff --git a/airflow-core/src/airflow/ui/src/queries/useParamStore.ts 
b/airflow-core/src/airflow/ui/src/queries/useParamStore.ts
index 39f40d7cae7..484197f92e1 100644
--- a/airflow-core/src/airflow/ui/src/queries/useParamStore.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useParamStore.ts
@@ -66,23 +66,47 @@ const createParamStore = () =>
           return {};
         }
 
-        const parsedConf = JSON.parse(confString) as JSON;
+        const parsedConf = JSON.parse(confString) as Record<string, unknown>;
+        const baseDict =
+          Object.keys(state.initialParamDict).length > 0 ? 
state.initialParamDict : state.paramsDict;
+
+        // Preserve a stable ordering of parameters (and thus sections in the 
trigger form)
+        // by following the order from the initial param dict when available.
+        const updatedParamsDictEntries: Array<[string, ParamSpec]> = [];
+        const inBase = new Set<string>(Object.keys(baseDict));
+
+        for (const [key, baseParam] of Object.entries(baseDict)) {
+          if (Object.hasOwn(parsedConf, key)) {
+            updatedParamsDictEntries.push([
+              key,
+              {
+                // eslint-disable-next-line unicorn/no-null
+                description: baseParam.description ?? null,
+                schema: baseParam.schema,
+                value: parsedConf[key],
+              },
+            ]);
+          }
+        }
 
-        const updatedParamsDict: ParamsSpec = Object.fromEntries(
-          Object.entries(parsedConf).map(([key, value]) => {
-            const existingParam = state.paramsDict[key];
+        // Append any extra keys that exist in the JSON but not in the base 
dict.
+        for (const [key, value] of Object.entries(parsedConf)) {
+          if (!inBase.has(key)) {
+            const existingParam = state.paramsDict[key] ?? 
state.initialParamDict[key];
 
-            return [
+            updatedParamsDictEntries.push([
               key,
               {
                 // eslint-disable-next-line unicorn/no-null
                 description: existingParam?.description ?? null,
                 schema: existingParam?.schema ?? paramPlaceholder.schema,
-                value: value as unknown,
+                value,
               },
-            ];
-          }),
-        );
+            ]);
+          }
+        }
+
+        const updatedParamsDict: ParamsSpec = 
Object.fromEntries(updatedParamsDictEntries);
 
         return { conf: confString, paramsDict: updatedParamsDict };
       }),
diff --git a/airflow-core/src/airflow/ui/src/queries/useTrigger.ts 
b/airflow-core/src/airflow/ui/src/queries/useTrigger.ts
index 450316055f6..8d1b24afb52 100644
--- a/airflow-core/src/airflow/ui/src/queries/useTrigger.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useTrigger.ts
@@ -29,7 +29,7 @@ import {
   UseGridServiceGetGridRunsKeyFn,
 } from "openapi/queries";
 import type { TriggerDagRunResponse } from "openapi/requests/types.gen";
-import type { DagRunTriggerParams } from 
"src/components/TriggerDag/TriggerDAGForm";
+import type { DagRunTriggerParams } from "src/components/TriggerDag/types";
 import { toaster } from "src/components/ui";
 
 export const useTrigger = ({ dagId, onSuccessConfirm }: { dagId: string; 
onSuccessConfirm: () => void }) => {

Reply via email to