Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
"title": "Dag Run Triggered"
}
},
"triggerAgainWithConfig": "Trigger again with this config",
"unpause": "Unpause {{dagDisplayName}} on trigger"
},
"trimText": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
"title": "已触发 Dag 执行"
}
},
"triggerAgainWithConfig": "使用此配置再次触发",
"unpause": "触发时取消暂停 {{dagDisplayName}}"
},
"trimText": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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}
Expand All @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,40 +45,30 @@ 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,
hasSchedule,
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 });
Expand All @@ -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 }));
Expand All @@ -116,7 +144,6 @@ const TriggerDAGForm = ({
const dataIntervalInvalid =
dataIntervalMode === "manual" &&
(noDataInterval || dayjs(dataIntervalStart).isAfter(dayjs(dataIntervalEnd)));

const onSubmit = (data: DagRunTriggerParams) => {
if (unpause && isPaused) {
togglePause({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> = ({
Expand All @@ -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);
Expand Down Expand Up @@ -129,6 +137,7 @@ const TriggerDAGModal: React.FC<TriggerDAGModalProps> = ({
isPaused={isPaused}
onClose={onClose}
open={open}
prefillConfig={prefillConfig}
/>
) : (
hasSchedule && dag && <RunBackfillForm dag={dag} onClose={onClose} />
Expand Down
36 changes: 36 additions & 0 deletions airflow-core/src/airflow/ui/src/components/TriggerDag/types.ts
Original file line number Diff line number Diff line change
@@ -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 }>;
42 changes: 33 additions & 9 deletions airflow-core/src/airflow/ui/src/queries/useParamStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}),
Expand Down
Loading
Loading