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 9fd0cc4efe940..eef5b4cd09df2 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
@@ -25,6 +25,17 @@
"requiredActions": "Required Actions",
"xcoms": "XComs"
},
+ "bulkAction": {
+ "delete": {
+ "taskInstance": {
+ "success": {
+ "description": "{{count}} task instances deleted successfully. Keys: {{keys}}",
+ "title": "Delete Task Instances Request Successful"
+ },
+ "warning": "This will remove all metadata related to the Task Instances, including task instance history and audit record."
+ }
+ }
+ },
"collapseAllExtra": "Collapse all extra json",
"collapseDetailsPanel": "Collapse Details Panel",
"createdAssetEvent_one": "Created Asset Event",
@@ -74,6 +85,7 @@
"dagWarnings": "Dag warnings/errors",
"defaultToGraphView": "Default to graph view",
"defaultToGridView": "Default to grid view",
+ "delete": "Delete",
"direction": "Direction",
"docs": {
"documentation": "Documentation",
@@ -183,6 +195,7 @@
"users": "Users"
},
"selectLanguage": "Select Language",
+ "selected": "Selected",
"showDetailsPanel": "Show Details Panel",
"signedInAs": "Signed in as",
"source": {
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json
index d54d3b6ba5a88..4246cbd6d2b91 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json
@@ -25,6 +25,17 @@
"requiredActions": "待回應的任務實例",
"xcoms": "XComs"
},
+ "bulkAction": {
+ "delete": {
+ "taskInstance": {
+ "success": {
+ "description": "已成功刪除 {{count}} 個任務實例。鍵:{{keys}}",
+ "title": "已提交刪除任務實例請求"
+ },
+ "warning": "這將刪除所有與任務實例相關的系統資料,包括歷史記錄和審計日誌。"
+ }
+ }
+ },
"collapseDetailsPanel": "收起詳細資訊",
"createdAssetEvent_one": "已建立資源事件",
"createdAssetEvent_other": "已建立資源事件",
@@ -73,6 +84,7 @@
"dagWarnings": "Dag 警告 / 錯誤",
"defaultToGraphView": "預設使用圖形視圖",
"defaultToGridView": "預設使用網格視圖",
+ "delete": "刪除",
"direction": "書寫方向",
"docs": {
"documentation": "文件",
diff --git a/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx b/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx
index a08b9744b51b0..a84f6771b6b0c 100644
--- a/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx
+++ b/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx
@@ -30,8 +30,8 @@ import { getColumns } from "./columns";
type Props = {
readonly affectedTasks?: TaskInstanceCollectionResponse;
- readonly note: DAGRunResponse["note"];
- readonly setNote: (value: string) => void;
+ readonly note?: DAGRunResponse["note"];
+ readonly setNote?: (value: string) => void;
};
// Table is in memory, pagination and sorting are disabled.
@@ -72,40 +72,42 @@ const ActionAccordion = ({ affectedTasks, note, setNote }: Props) => {
) : undefined}
-
-
- {translate("note.label")}
-
-
- ) => setNote(event.target.value)}
- value={note ?? ""}
- >
-
+
+ {translate("note.label")}
+
+
+ ) => setNote(event.target.value)}
+ value={note ?? ""}
>
- {Boolean(note) ? (
- {note}
- ) : (
- {translate("note.placeholder")}
- )}
-
-
-
-
-
+
+ {Boolean(note) ? (
+ {note}
+ ) : (
+ {translate("note.placeholder")}
+ )}
+
+
+
+
+
+ ) : undefined}
);
};
diff --git a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstances/MarkTaskInstancesAsButton.tsx b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstances/MarkTaskInstancesAsButton.tsx
new file mode 100644
index 0000000000000..3f10d219fd96a
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstances/MarkTaskInstancesAsButton.tsx
@@ -0,0 +1,106 @@
+/*!
+ * 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 { useDisclosure } from "@chakra-ui/react";
+import type { TaskInstanceResponse, TaskInstanceState } from "openapi-gen/requests/types.gen";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { MdArrowDropDown } from "react-icons/md";
+
+import { StateBadge } from "src/components/StateBadge";
+import { Menu, Tooltip } from "src/components/ui";
+import ActionButton from "src/components/ui/ActionButton";
+
+import PatchTaskInstancesDialog from "./MarkTaskInstancesAsDialog";
+
+type Props = {
+ readonly clearSelections: () => void;
+ readonly dagId: string;
+ readonly dagRunId: string;
+ readonly patchKeys: Array;
+};
+
+const MarkTaskInstancesAsButton = ({ clearSelections, dagId, dagRunId, patchKeys }: Props) => {
+ const { onClose, onOpen, open } = useDisclosure();
+ const [selectedState, setSelectedState] = useState("success");
+
+ const allowedStates: Array = ["success", "failed"];
+
+ const { t: translate } = useTranslation();
+
+ if (patchKeys.length === 0) {
+ return undefined;
+ }
+
+ const type = translate("common:taskInstance_other");
+ const patchButtonText = translate("dags:runAndTaskActions.markAs.button", { type });
+
+ return (
+ <>
+
+
+ }
+ text={patchButtonText}
+ variant="outline"
+ withText
+ />
+
+
+ {allowedStates.map((menuState) => {
+ const content = translate(
+ `dags:runAndTaskActions.markAs.buttonTooltip.${menuState === "success" ? "success" : "failed"}`,
+ );
+
+ return (
+
+ {
+ setSelectedState(menuState);
+ onOpen();
+ }}
+ value={menuState}
+ >
+
+ {translate(`common:states.${menuState}`)}
+
+
+
+ );
+ })}
+
+
+
+ >
+ );
+};
+
+export default MarkTaskInstancesAsButton;
diff --git a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstances/MarkTaskInstancesAsDialog.tsx b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstances/MarkTaskInstancesAsDialog.tsx
new file mode 100644
index 0000000000000..f1b123ce92e27
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstances/MarkTaskInstancesAsDialog.tsx
@@ -0,0 +1,109 @@
+/*!
+ * 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 { Button, Flex, Heading, VStack } from "@chakra-ui/react";
+import type {
+ TaskInstanceCollectionResponse,
+ TaskInstanceResponse,
+ TaskInstanceState,
+} from "openapi-gen/requests/types.gen";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+
+import { ActionAccordion } from "src/components/ActionAccordion";
+import { StateBadge } from "src/components/StateBadge";
+import { Dialog } from "src/components/ui/Dialog";
+import { useBulkPatchTaskInstances } from "src/queries/useBulkPatchTaskInstances";
+
+type Props = {
+ readonly clearSelections: () => void;
+ readonly dagId: string;
+ readonly dagRunId: string;
+ readonly onClose: () => void;
+ readonly open: boolean;
+ readonly patchKeys: Array;
+ readonly selectedState: TaskInstanceState;
+};
+
+const MarkTaskInstancesAsDialog = ({
+ clearSelections,
+ dagId,
+ dagRunId,
+ onClose,
+ open,
+ patchKeys,
+ selectedState,
+}: Props) => {
+ const [note, setNote] = useState();
+
+ const { isPending, patchTaskInstances } = useBulkPatchTaskInstances({
+ dagId,
+ dagRunId,
+ onSuccessConfirm: () => {
+ clearSelections();
+ onClose();
+ },
+ });
+ const { t: translate } = useTranslation();
+
+ const affectedTasks = {
+ task_instances: patchKeys,
+ total_entries: patchKeys.length,
+ } as TaskInstanceCollectionResponse;
+
+ const handlePatch = (state: TaskInstanceState) => {
+ const actionValue = state === "failed" ? "set_failed" : "set_success";
+
+ patchTaskInstances(patchKeys, actionValue, { note });
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+
+ {translate("dags:runAndTaskActions.markAs.title", {
+ state: selectedState,
+ type: translate("common:taskInstance_other"),
+ })}
+ :
+ {" "}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MarkTaskInstancesAsDialog;
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstances/DeleteTaskInstancesButton.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstances/DeleteTaskInstancesButton.tsx
new file mode 100644
index 0000000000000..5bf4e987eab34
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstances/DeleteTaskInstancesButton.tsx
@@ -0,0 +1,99 @@
+/*!
+ * 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 { useDisclosure } from "@chakra-ui/react";
+import { Button, Flex, Heading, Text } from "@chakra-ui/react";
+import type { TaskInstanceCollectionResponse, TaskInstanceResponse } from "openapi-gen/requests/types.gen";
+import { useTranslation } from "react-i18next";
+import { FiTrash2 } from "react-icons/fi";
+
+import { ActionAccordion } from "src/components/ActionAccordion";
+import ActionButton from "src/components/ui/ActionButton";
+import { Dialog } from "src/components/ui/Dialog";
+import { useBulkDeleteTaskInstances } from "src/queries/useBulkDeleteTaskInstances";
+
+type Props = {
+ readonly clearSelections: () => void;
+ readonly dagId: string;
+ readonly dagRunId: string;
+ readonly deleteKeys: Array;
+};
+
+const DeleteTaskInstancesButton = ({ clearSelections, dagId, dagRunId, deleteKeys }: Props) => {
+ const { onClose, onOpen, open } = useDisclosure();
+ const { deleteTaskInstances, isPending } = useBulkDeleteTaskInstances({
+ dagId,
+ dagRunId,
+ onSuccessConfirm: () => {
+ clearSelections();
+ onClose();
+ },
+ });
+ const { t: translate } = useTranslation();
+
+ if (deleteKeys.length === 0) {
+ return undefined;
+ }
+
+ const type = translate("common:taskInstance_other");
+ const title = translate("dags:runAndTaskActions.delete.dialog.title", { type });
+ const warningText = translate("common:bulkAction.delete.taskInstance.warning", { type });
+ const deleteButtonText = translate("dags:runAndTaskActions.delete.button", { type });
+
+ const affectedTasks = {
+ task_instances: deleteKeys,
+ total_entries: deleteKeys.length,
+ } as TaskInstanceCollectionResponse;
+
+ return (
+ <>
+ }
+ onClick={onOpen}
+ text={deleteButtonText}
+ variant="outline"
+ withText
+ />
+
+
+
+
+ {title}
+
+ {warningText}
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default DeleteTaskInstancesButton;
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
index f4e0e14aca6c5..33024755e32b8 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
@@ -30,19 +30,27 @@ import type { TaskInstanceResponse } from "openapi/requests/types.gen";
import { ClearTaskInstanceButton } from "src/components/Clear";
import { DagVersion } from "src/components/DagVersion";
import { DataTable } from "src/components/DataTable";
+import { type GetColumnsParams, useRowSelection } from "src/components/DataTable/useRowSelection";
import { useTableURLState } from "src/components/DataTable/useTableUrlState";
import { ErrorAlert } from "src/components/ErrorAlert";
import { MarkTaskInstanceAsButton } from "src/components/MarkAs";
+import MarkTaskInstancesAsButton from "src/components/MarkAs/TaskInstances/MarkTaskInstancesAsButton";
import { StateBadge } from "src/components/StateBadge";
import Time from "src/components/Time";
import { TruncatedText } from "src/components/TruncatedText";
+import { ActionBar } from "src/components/ui/ActionBar";
+import { Checkbox } from "src/components/ui/Checkbox";
+import { Tooltip } from "src/components/ui/Tooltip";
import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams";
import { useAutoRefresh, isStatePending, renderDuration } from "src/utils";
import { getTaskInstanceLink } from "src/utils/links";
import DeleteTaskInstanceButton from "./DeleteTaskInstanceButton";
+import DeleteTaskInstancesButton from "./DeleteTaskInstancesButton";
import { TaskInstancesFilter } from "./TaskInstancesFilter";
+const SEPARATOR = "SEPARATOR";
+
type TaskInstanceRow = { row: { original: TaskInstanceResponse } };
const {
@@ -64,9 +72,13 @@ const {
TRY_NUMBER: TRY_NUMBER_PARAM,
}: SearchParamsKeysType = SearchParamsKeys;
-const taskInstanceColumns = ({
+const getColumns = ({
+ allRowsSelected,
dagId,
+ onRowSelect,
+ onSelectAll,
runId,
+ selectedRows,
taskId,
translate,
}: {
@@ -74,7 +86,37 @@ const taskInstanceColumns = ({
runId?: string;
taskId?: string;
translate: TFunction;
-}): Array> => [
+} & GetColumnsParams): Array> => [
+ {
+ accessorKey: "select",
+ cell: ({ row }: TaskInstanceRow) => (
+
+ onRowSelect(
+ `${row.original.dag_run_id}${SEPARATOR}${row.original.task_id}${SEPARATOR}${row.original.map_index}`,
+ Boolean(event.checked),
+ )
+ }
+ />
+ ),
+ enableSorting: false,
+ header: () => (
+ onSelectAll(Boolean(event.checked))}
+ />
+ ),
+ meta: {
+ skeletonWidth: 10,
+ },
+ },
...(Boolean(dagId)
? []
: [
@@ -291,15 +333,36 @@ export const TaskInstances = () => {
},
);
+ const { allRowsSelected, clearSelections, handleRowSelect, handleSelectAll, selectedRows } =
+ useRowSelection({
+ data: data?.task_instances,
+ getKey: (taskInstance) =>
+ `${taskInstance.dag_run_id}${SEPARATOR}${taskInstance.task_id}${SEPARATOR}${taskInstance.map_index}`,
+ });
+
const columns = useMemo(
() =>
- taskInstanceColumns({
+ getColumns({
+ allRowsSelected,
dagId,
+ onRowSelect: handleRowSelect,
+ onSelectAll: handleSelectAll,
runId,
+ selectedRows,
taskId: Boolean(groupId) ? undefined : taskId,
translate,
}),
- [dagId, runId, groupId, taskId, translate],
+ [
+ allRowsSelected,
+ dagId,
+ groupId,
+ handleRowSelect,
+ handleSelectAll,
+ runId,
+ selectedRows,
+ taskId,
+ translate,
+ ],
);
return (
@@ -315,6 +378,63 @@ export const TaskInstances = () => {
onStateChange={setTableURLState}
total={data?.total_entries}
/>
+
+
+
+ {selectedRows.size} {translate("common:selected")}
+
+
+
+ {
+ const [dagRunId, currentTaskId, mapIndex] = id.split(SEPARATOR);
+
+ return data?.task_instances.find(
+ (ti) =>
+ ti.dag_run_id === dagRunId &&
+ ti.task_id === currentTaskId &&
+ ti.map_index === Number(mapIndex ?? -1),
+ );
+ })
+ .filter(Boolean) as Array
+ }
+ />
+
+
+ {
+ const [dagRunId, currentTaskId, mapIndex] = id.split(SEPARATOR);
+
+ return data?.task_instances.find(
+ (ti) =>
+ ti.dag_run_id === dagRunId &&
+ ti.task_id === currentTaskId &&
+ ti.map_index === Number(mapIndex ?? -1),
+ );
+ })
+ .filter(Boolean) as Array
+ }
+ />
+
+
+
+
>
);
};
diff --git a/airflow-core/src/airflow/ui/src/queries/useBulkDeleteTaskInstances.ts b/airflow-core/src/airflow/ui/src/queries/useBulkDeleteTaskInstances.ts
new file mode 100644
index 0000000000000..1f1cd9a4f82af
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/useBulkDeleteTaskInstances.ts
@@ -0,0 +1,110 @@
+/*!
+ * 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 { useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+
+import {
+ useTaskInstanceServiceGetTaskInstancesKey,
+ useTaskInstanceServiceBulkTaskInstances,
+ UseGridServiceGetGridTiSummariesKeyFn,
+} from "openapi/queries";
+import type { TaskInstanceResponse } from "openapi/requests";
+import { toaster } from "src/components/ui";
+
+type Props = {
+ readonly dagId?: string;
+ readonly dagRunId?: string;
+ readonly onSuccessConfirm: VoidFunction;
+};
+
+export const useBulkDeleteTaskInstances = ({ dagId, dagRunId, onSuccessConfirm }: Props) => {
+ const queryClient = useQueryClient();
+ const [error, setError] = useState(undefined);
+ const { t: translate } = useTranslation("common");
+
+ const onSuccess = async (responseData: { delete?: { errors: Array; success: Array } }) => {
+ const queryKeys = [
+ [useTaskInstanceServiceGetTaskInstancesKey],
+ dagId === undefined || dagRunId === undefined
+ ? []
+ : [UseGridServiceGetGridTiSummariesKeyFn({ dagId, runId: dagRunId })],
+ ];
+
+ await Promise.all(queryKeys.map((key) => queryClient.invalidateQueries({ queryKey: key })));
+
+ if (responseData.delete) {
+ const { errors, success } = responseData.delete;
+
+ if (Array.isArray(errors) && errors.length > 0) {
+ const apiError = errors[0] as { error: string };
+
+ setError({
+ body: { detail: apiError.error },
+ });
+ } else if (Array.isArray(success) && success.length > 0) {
+ toaster.create({
+ description: translate("bulkAction.delete.taskInstance.success.description", {
+ count: success.length,
+ keys: success.join(", "),
+ }),
+ title: translate("bulkAction.delete.taskInstance.success.title"),
+ type: "success",
+ });
+ onSuccessConfirm();
+ }
+ }
+ };
+
+ const onError = (_error: unknown) => {
+ setError(_error);
+ };
+
+ const { isPending, mutate } = useTaskInstanceServiceBulkTaskInstances({
+ onError,
+ onSuccess,
+ });
+
+ const deleteTaskInstances = (entities: Array) => {
+ setError(undefined);
+
+ const isSingleDagRun =
+ Boolean(dagId) && Boolean(dagRunId) && dagId !== undefined && dagRunId !== undefined;
+
+ mutate({
+ dagId: isSingleDagRun ? dagId : "~",
+ dagRunId: isSingleDagRun ? dagRunId : "~",
+ requestBody: {
+ actions: [
+ {
+ action: "delete",
+ entities: entities.map((ti) => ({
+ dag_id: isSingleDagRun ? undefined : ti.dag_id,
+ dag_run_id: isSingleDagRun ? undefined : ti.dag_run_id,
+ map_index: ti.map_index,
+ task_id: ti.task_id,
+ })),
+ },
+ ],
+ },
+ });
+ };
+
+ return { deleteTaskInstances, error, isPending };
+};
diff --git a/airflow-core/src/airflow/ui/src/queries/useBulkPatchTaskInstances.ts b/airflow-core/src/airflow/ui/src/queries/useBulkPatchTaskInstances.ts
new file mode 100644
index 0000000000000..25843ece60fb9
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/useBulkPatchTaskInstances.ts
@@ -0,0 +1,136 @@
+/*!
+ * 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 { useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+
+import {
+ useTaskInstanceServiceGetTaskInstancesKey,
+ useTaskInstanceServiceBulkTaskInstances,
+ UseGridServiceGetGridTiSummariesKeyFn,
+} from "openapi/queries";
+import type { TaskInstanceResponse } from "openapi/requests";
+import { toaster } from "src/components/ui";
+
+type Props = {
+ readonly dagId?: string;
+ readonly dagRunId?: string;
+ readonly onSuccessConfirm: VoidFunction;
+};
+
+export const useBulkPatchTaskInstances = ({ dagId, dagRunId, onSuccessConfirm }: Props) => {
+ const queryClient = useQueryClient();
+ const [error, setError] = useState(undefined);
+ const { t: translate } = useTranslation("common");
+
+ const onSuccess = async (responseData: { patch?: { errors: Array; success: Array } }) => {
+ const queryKeys = [
+ [useTaskInstanceServiceGetTaskInstancesKey],
+ dagId === undefined || dagRunId === undefined
+ ? []
+ : [UseGridServiceGetGridTiSummariesKeyFn({ dagId, runId: dagRunId })],
+ ];
+
+ await Promise.all(queryKeys.map((key) => queryClient.invalidateQueries({ queryKey: key })));
+
+ if (responseData.patch) {
+ const { errors, success } = responseData.patch;
+
+ if (Array.isArray(errors) && errors.length > 0) {
+ const apiError = errors[0] as { error: string };
+
+ setError({
+ body: { detail: apiError.error },
+ });
+ } else if (Array.isArray(success) && success.length > 0) {
+ toaster.create({
+ description: translate("bulkAction.patch.taskInstance.success.description", {
+ count: success.length,
+ keys: success.join(", "),
+ }),
+ title: translate("bulkAction.patch.taskInstance.success.title"),
+ type: "success",
+ });
+ onSuccessConfirm();
+ }
+ }
+ };
+
+ const onError = (_error: unknown) => {
+ setError(_error);
+ };
+
+ const getNewState = (action: string) => {
+ switch (action) {
+ case "set_failed":
+ return "failed" as const;
+ case "set_success":
+ return "success" as const;
+ default:
+ return undefined;
+ }
+ };
+
+ const { isPending, mutate } = useTaskInstanceServiceBulkTaskInstances({
+ onError,
+ onSuccess,
+ });
+
+ const patchTaskInstances = (
+ entities: Array,
+ selectedAction: string,
+ options: {
+ include_downstream?: boolean;
+ include_future?: boolean;
+ include_past?: boolean;
+ include_upstream?: boolean;
+ note?: string | null;
+ } = {},
+ ) => {
+ const newState = getNewState(selectedAction);
+ const isSingleDagRun =
+ Boolean(dagId) && Boolean(dagRunId) && dagId !== undefined && dagRunId !== undefined;
+
+ mutate({
+ dagId: isSingleDagRun ? dagId : "~",
+ dagRunId: isSingleDagRun ? dagRunId : "~",
+ requestBody: {
+ actions: [
+ {
+ action: "update",
+ entities: entities.map((ti) => ({
+ dag_id: isSingleDagRun ? undefined : ti.dag_id,
+ dag_run_id: isSingleDagRun ? undefined : ti.dag_run_id,
+ include_downstream: options.include_downstream,
+ include_future: options.include_future,
+ include_past: options.include_past,
+ include_upstream: options.include_upstream,
+ map_index: ti.map_index,
+ new_state: newState,
+ note: options.note,
+ task_id: ti.task_id,
+ })),
+ },
+ ],
+ },
+ });
+ };
+
+ return { error, isPending, patchTaskInstances };
+};