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 }) => {