diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index 8dda576063e0b..9082320cff85e 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -208,6 +208,7 @@ }, "task_one": "Task", "task_other": "Tasks", + "taskGroup": "Task Group", "taskId": "Task ID", "taskInstance": { "dagVersion": "Dag Version", diff --git a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearGroupTaskInstanceDialog.tsx b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearGroupTaskInstanceDialog.tsx new file mode 100644 index 0000000000000..22e7eff0d4dc2 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearGroupTaskInstanceDialog.tsx @@ -0,0 +1,196 @@ +/*! + * 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. + */ +import { Flex, Heading, VStack } from "@chakra-ui/react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CgRedo } from "react-icons/cg"; +import { useParams } from "react-router-dom"; + +import { useDagServiceGetDagDetails, useTaskInstanceServiceGetTaskInstances } from "openapi/queries"; +import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen"; +import { ActionAccordion } from "src/components/ActionAccordion"; +import { Button, Dialog, Checkbox } from "src/components/ui"; +import SegmentedControl from "src/components/ui/SegmentedControl"; +import { useClearTaskInstances } from "src/queries/useClearTaskInstances"; +import { useClearTaskInstancesDryRun } from "src/queries/useClearTaskInstancesDryRun"; + +type Props = { + readonly onClose: () => void; + readonly open: boolean; + readonly taskInstance: LightGridTaskInstanceSummary; +}; + +export const ClearGroupTaskInstanceDialog = ({ onClose, open, taskInstance }: Props) => { + const { t: translate } = useTranslation(); + const { dagId = "", runId = "" } = useParams(); + const groupId = taskInstance.task_id; + + const { isPending, mutate } = useClearTaskInstances({ + dagId, + dagRunId: runId, + onSuccessConfirm: onClose, + }); + + const [selectedOptions, setSelectedOptions] = useState>([]); + + const onlyFailed = selectedOptions.includes("onlyFailed"); + const past = selectedOptions.includes("past"); + const future = selectedOptions.includes("future"); + const upstream = selectedOptions.includes("upstream"); + const downstream = selectedOptions.includes("downstream"); + const [runOnLatestVersion, setRunOnLatestVersion] = useState(false); + + const [note, setNote] = useState(""); + + const { data: dagDetails } = useDagServiceGetDagDetails({ + dagId, + }); + + const { data: groupTaskInstances } = useTaskInstanceServiceGetTaskInstances( + { + dagId, + dagRunId: runId, + taskDisplayNamePattern: groupId, + }, + undefined, + { + enabled: open, + }, + ); + + const groupTaskIds = groupTaskInstances?.task_instances.map((ti) => ti.task_id) ?? []; + + const { data } = useClearTaskInstancesDryRun({ + dagId, + options: { + enabled: open && groupTaskIds.length > 0, + refetchOnMount: "always", + }, + requestBody: { + dag_run_id: runId, + include_downstream: downstream, + include_future: future, + include_past: past, + include_upstream: upstream, + only_failed: onlyFailed, + run_on_latest_version: runOnLatestVersion, + task_ids: groupTaskIds, + }, + }); + + const affectedTasks = data ?? { + task_instances: [], + total_entries: 0, + }; + + const shouldShowBundleVersionOption = + dagDetails?.bundle_version !== null && dagDetails?.bundle_version !== ""; + + return ( + + + + + + + {translate("dags:runAndTaskActions.clear.title", { + type: translate("taskInstance", { count: affectedTasks.total_entries }), + })} + : + {" "} + {groupId} + + + + + + + + + + + + + {shouldShowBundleVersionOption ? ( + setRunOnLatestVersion(Boolean(event.checked))} + > + {translate("dags:runAndTaskActions.options.runOnLatestVersion")} + + ) : undefined} + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx index 5ce1edf0f3d3a..5e8e27ac75779 100644 --- a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx +++ b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx @@ -21,21 +21,29 @@ import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { CgRedo } from "react-icons/cg"; -import type { TaskInstanceResponse } from "openapi/requests/types.gen"; +import type { LightGridTaskInstanceSummary, TaskInstanceResponse } from "openapi/requests/types.gen"; +import { ClearGroupTaskInstanceDialog } from "src/components/Clear/TaskInstance/ClearGroupTaskInstanceDialog"; import { Tooltip } from "src/components/ui"; import ActionButton from "src/components/ui/ActionButton"; import ClearTaskInstanceDialog from "./ClearTaskInstanceDialog"; type Props = { + readonly groupTaskInstance?: LightGridTaskInstanceSummary; readonly isHotkeyEnabled?: boolean; - readonly taskInstance: TaskInstanceResponse; + readonly taskInstance?: TaskInstanceResponse; readonly withText?: boolean; }; -const ClearTaskInstanceButton = ({ isHotkeyEnabled = false, taskInstance, withText = true }: Props) => { +const ClearTaskInstanceButton = ({ + groupTaskInstance, + isHotkeyEnabled = false, + taskInstance, + withText = true, +}: Props) => { const { onClose, onOpen, open } = useDisclosure(); const { t: translate } = useTranslation(); + const isGroup = groupTaskInstance && !taskInstance; useHotkeys( "shift+c", @@ -59,11 +67,17 @@ const ClearTaskInstanceButton = ({ isHotkeyEnabled = false, taskInstance, withTe })} icon={} onClick={onOpen} - text={translate("dags:runAndTaskActions.clear.button", { type: translate("taskInstance_one") })} + text={translate("dags:runAndTaskActions.clear.button", { + type: translate(isGroup ? "taskGroup" : "taskInstance_one"), + })} withText={withText} /> - {open ? ( + {open && isGroup ? ( + + ) : undefined} + + {open && !isGroup && taskInstance ? ( ) : undefined} diff --git a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx index c9150ac2dbda1..feedee26b333f 100644 --- a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx +++ b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx @@ -28,10 +28,10 @@ import { isStatePending, useAutoRefresh } from "src/utils"; import { Header } from "./Header"; export const GroupTaskInstance = () => { - const { dagId = "", runId = "", taskId = "" } = useParams(); + const { dagId = "", groupId = "", runId = "" } = useParams(); const { t: translate } = useTranslation("dag"); const { data: gridTISummaries } = useGridTiSummaries({ dagId, runId }); - const taskInstance = gridTISummaries?.task_instances.find((ti) => ti.task_id === taskId); + const taskInstance = gridTISummaries?.task_instances.find((ti) => ti.task_id === groupId); const refetchInterval = useAutoRefresh({ dagId }); diff --git a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx index 9bfd54bfda41d..ec72c12cbb943 100644 --- a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx @@ -22,6 +22,7 @@ import { useTranslation } from "react-i18next"; import { MdOutlineTask } from "react-icons/md"; import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen"; +import { ClearTaskInstanceButton } from "src/components/Clear"; import { HeaderCard } from "src/components/HeaderCard"; import Time from "src/components/Time"; import { getDuration } from "src/utils"; @@ -56,6 +57,7 @@ export const Header = ({ return ( } icon={} isRefreshing={isRefreshing} state={taskInstance.state}