From ef33eb3574b10365bbb963ec0565239dadd15251 Mon Sep 17 00:00:00 2001 From: GUAN MING Date: Sun, 10 Aug 2025 16:16:36 +0800 Subject: [PATCH 1/2] Add clear TI for task group --- .../ClearGroupTaskInstanceDialog.tsx | 196 ++++++++++++++++++ .../TaskInstance/ClearTaskInstanceButton.tsx | 19 +- .../GroupTaskInstance/GroupTaskInstance.tsx | 4 +- .../ui/src/pages/GroupTaskInstance/Header.tsx | 2 + 4 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearGroupTaskInstanceDialog.tsx 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..23f22caff760c 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,19 +21,26 @@ 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(); @@ -63,7 +70,11 @@ const ClearTaskInstanceButton = ({ isHotkeyEnabled = false, taskInstance, withTe withText={withText} /> - {open ? ( + {open && groupTaskInstance && !taskInstance ? ( + + ) : undefined} + + {open && taskInstance && !groupTaskInstance ? ( ) : 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} From 3793dff8fd3bb4e59f3c4bd004f588de0f3ce92c Mon Sep 17 00:00:00 2001 From: GUAN MING Date: Wed, 13 Aug 2025 15:23:55 +0800 Subject: [PATCH 2/2] Simplify clear button display logic --- .../src/airflow/ui/public/i18n/locales/en/common.json | 1 + .../Clear/TaskInstance/ClearTaskInstanceButton.tsx | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) 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/ClearTaskInstanceButton.tsx b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx index 23f22caff760c..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 @@ -43,6 +43,7 @@ const ClearTaskInstanceButton = ({ }: Props) => { const { onClose, onOpen, open } = useDisclosure(); const { t: translate } = useTranslation(); + const isGroup = groupTaskInstance && !taskInstance; useHotkeys( "shift+c", @@ -66,15 +67,17 @@ const ClearTaskInstanceButton = ({ })} 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 && groupTaskInstance && !taskInstance ? ( + {open && isGroup ? ( ) : undefined} - {open && taskInstance && !groupTaskInstance ? ( + {open && !isGroup && taskInstance ? ( ) : undefined}