You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ep...@apache.org on 2023/04/14 18:55:58 UTC
[airflow] 03/08: Improve task & run actions ux in grid view (#30373)
This is an automated email from the ASF dual-hosted git repository.
ephraimanierobi pushed a commit to branch v2-6-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 2a4fd5bdce4ae99a93a2b9ec69860f4a181b6990
Author: Brent Bovenzi <br...@astronomer.io>
AuthorDate: Fri Apr 14 12:51:53 2023 -0400
Improve task & run actions ux in grid view (#30373)
* update run clear+mark, update task clear
* add mark as tasks and include list of affected tasks
* Add support for mapped tasks, add shared modal component
* Clean up styling, restore warning for past/future tg clear
(cherry picked from commit c5b685e88dd6ecf56d96ef4fefa6c409f28e2b22)
---
airflow/www/static/js/api/index.ts | 4 +-
airflow/www/static/js/api/useClearTask.ts | 7 +-
.../api/{useClearTask.ts => useClearTaskDryRun.ts} | 70 +++---
airflow/www/static/js/api/useMarkFailedTask.ts | 8 +-
airflow/www/static/js/api/useMarkSuccessTask.ts | 8 +-
...{useConfirmMarkTask.ts => useMarkTaskDryRun.ts} | 45 ++--
airflow/www/static/js/components/ConfirmDialog.tsx | 103 --------
.../www/static/js/dag/details/dagRun/ClearRun.tsx | 81 ++++---
.../static/js/dag/details/dagRun/MarkFailedRun.tsx | 73 ------
.../www/static/js/dag/details/dagRun/MarkRunAs.tsx | 90 +++++++
.../js/dag/details/dagRun/MarkSuccessRun.tsx | 77 ------
.../www/static/js/dag/details/dagRun/QueueRun.tsx | 76 ------
airflow/www/static/js/dag/details/dagRun/index.tsx | 19 --
airflow/www/static/js/dag/details/index.tsx | 37 ++-
.../static/js/dag/details/taskInstance/index.tsx | 20 --
.../taskInstance/taskActions/ActionButton.tsx | 4 +-
.../taskInstance/taskActions/ActionModal.tsx | 112 +++++++++
.../dag/details/taskInstance/taskActions/Clear.tsx | 174 --------------
.../taskInstance/taskActions/ClearInstance.tsx | 235 ++++++++++++++++++
.../taskInstance/taskActions/MarkFailed.tsx | 141 -----------
.../taskInstance/taskActions/MarkInstanceAs.tsx | 267 +++++++++++++++++++++
.../taskInstance/taskActions/MarkSuccess.tsx | 141 -----------
.../dag/details/taskInstance/taskActions/index.tsx | 84 -------
.../dag/details/taskInstance/taskActions/types.ts | 29 ---
24 files changed, 866 insertions(+), 1039 deletions(-)
diff --git a/airflow/www/static/js/api/index.ts b/airflow/www/static/js/api/index.ts
index 2bbfe10b66..018f4e97b6 100644
--- a/airflow/www/static/js/api/index.ts
+++ b/airflow/www/static/js/api/index.ts
@@ -28,7 +28,7 @@ import useClearTask from "./useClearTask";
import useMarkFailedTask from "./useMarkFailedTask";
import useMarkSuccessTask from "./useMarkSuccessTask";
import useExtraLinks from "./useExtraLinks";
-import useConfirmMarkTask from "./useConfirmMarkTask";
+import useMarkTaskDryRun from "./useMarkTaskDryRun";
import useGraphData from "./useGraphData";
import useGridData from "./useGridData";
import useMappedInstances from "./useMappedInstances";
@@ -50,7 +50,7 @@ axios.defaults.headers.common.Accept = "application/json";
export {
useClearRun,
useClearTask,
- useConfirmMarkTask,
+ useMarkTaskDryRun,
useDataset,
useDatasetDependencies,
useDatasetEvents,
diff --git a/airflow/www/static/js/api/useClearTask.ts b/airflow/www/static/js/api/useClearTask.ts
index ebe3b14b28..b4f80e5a7b 100644
--- a/airflow/www/static/js/api/useClearTask.ts
+++ b/airflow/www/static/js/api/useClearTask.ts
@@ -63,7 +63,7 @@ export default function useClearTask({
recursive: boolean;
failed: boolean;
confirmed: boolean;
- mapIndexes: number[];
+ mapIndexes?: number[];
}) => {
const params = new URLSearchParamsWrapper({
csrf_token: csrfToken,
@@ -105,10 +105,13 @@ export default function useClearTask({
runId,
taskId,
]);
+ queryClient.invalidateQueries(["clearTask", dagId, runId, taskId]);
startRefresh();
}
},
- onError: (error: Error) => errorToast({ error }),
+ onError: (error: Error, { confirmed }) => {
+ if (confirmed) errorToast({ error });
+ },
}
);
}
diff --git a/airflow/www/static/js/api/useClearTask.ts b/airflow/www/static/js/api/useClearTaskDryRun.ts
similarity index 65%
copy from airflow/www/static/js/api/useClearTask.ts
copy to airflow/www/static/js/api/useClearTaskDryRun.ts
index ebe3b14b28..cea46723f4 100644
--- a/airflow/www/static/js/api/useClearTask.ts
+++ b/airflow/www/static/js/api/useClearTaskDryRun.ts
@@ -18,58 +18,60 @@
*/
import axios, { AxiosResponse } from "axios";
-import { useMutation, useQueryClient } from "react-query";
+import { useQuery } from "react-query";
import URLSearchParamsWrapper from "src/utils/URLSearchParamWrapper";
import { getMetaValue } from "../utils";
-import { useAutoRefresh } from "../context/autorefresh";
-import useErrorToast from "../utils/useErrorToast";
const csrfToken = getMetaValue("csrf_token");
const clearUrl = getMetaValue("clear_url");
-export default function useClearTask({
+const useClearTaskDryRun = ({
dagId,
runId,
taskId,
executionDate,
isGroup,
+ past,
+ future,
+ upstream,
+ downstream,
+ recursive,
+ failed,
+ mapIndexes = [],
}: {
dagId: string;
runId: string;
taskId: string;
executionDate: string;
isGroup: boolean;
-}) {
- const queryClient = useQueryClient();
- const errorToast = useErrorToast();
- const { startRefresh } = useAutoRefresh();
-
- return useMutation(
- ["clearTask", dagId, runId, taskId],
- ({
+ past: boolean;
+ future: boolean;
+ upstream: boolean;
+ downstream: boolean;
+ recursive: boolean;
+ failed: boolean;
+ mapIndexes?: number[];
+}) =>
+ useQuery(
+ [
+ "clearTask",
+ dagId,
+ runId,
+ taskId,
+ mapIndexes,
past,
future,
upstream,
downstream,
recursive,
failed,
- confirmed,
- mapIndexes = [],
- }: {
- past: boolean;
- future: boolean;
- upstream: boolean;
- downstream: boolean;
- recursive: boolean;
- failed: boolean;
- confirmed: boolean;
- mapIndexes: number[];
- }) => {
+ ],
+ () => {
const params = new URLSearchParamsWrapper({
csrf_token: csrfToken,
dag_id: dagId,
dag_run_id: runId,
- confirmed,
+ confirmed: false,
execution_date: executionDate,
past,
future,
@@ -94,21 +96,7 @@ export default function useClearTask({
"Content-Type": "application/x-www-form-urlencoded",
},
});
- },
- {
- onSuccess: (_, { confirmed }) => {
- if (confirmed) {
- queryClient.invalidateQueries("gridData");
- queryClient.invalidateQueries([
- "mappedInstances",
- dagId,
- runId,
- taskId,
- ]);
- startRefresh();
- }
- },
- onError: (error: Error) => errorToast({ error }),
}
);
-}
+
+export default useClearTaskDryRun;
diff --git a/airflow/www/static/js/api/useMarkFailedTask.ts b/airflow/www/static/js/api/useMarkFailedTask.ts
index 23bbda60ab..c74c86e632 100644
--- a/airflow/www/static/js/api/useMarkFailedTask.ts
+++ b/airflow/www/static/js/api/useMarkFailedTask.ts
@@ -52,7 +52,7 @@ export default function useMarkFailedTask({
future: boolean;
upstream: boolean;
downstream: boolean;
- mapIndexes: number[];
+ mapIndexes?: number[];
}) => {
const params = new URLSearchParamsWrapper({
csrf_token: csrfToken,
@@ -85,6 +85,12 @@ export default function useMarkFailedTask({
runId,
taskId,
]);
+ queryClient.invalidateQueries([
+ "confirmStateChange",
+ dagId,
+ runId,
+ taskId,
+ ]);
startRefresh();
},
onError: (error: Error) => errorToast({ error }),
diff --git a/airflow/www/static/js/api/useMarkSuccessTask.ts b/airflow/www/static/js/api/useMarkSuccessTask.ts
index 2605a92526..4c18aa9dc2 100644
--- a/airflow/www/static/js/api/useMarkSuccessTask.ts
+++ b/airflow/www/static/js/api/useMarkSuccessTask.ts
@@ -52,7 +52,7 @@ export default function useMarkSuccessTask({
future: boolean;
upstream: boolean;
downstream: boolean;
- mapIndexes: number[];
+ mapIndexes?: number[];
}) => {
const params = new URLSearchParamsWrapper({
csrf_token: csrfToken,
@@ -85,6 +85,12 @@ export default function useMarkSuccessTask({
runId,
taskId,
]);
+ queryClient.invalidateQueries([
+ "confirmStateChange",
+ dagId,
+ runId,
+ taskId,
+ ]);
startRefresh();
},
onError: (error: Error) => errorToast({ error }),
diff --git a/airflow/www/static/js/api/useConfirmMarkTask.ts b/airflow/www/static/js/api/useMarkTaskDryRun.ts
similarity index 76%
rename from airflow/www/static/js/api/useConfirmMarkTask.ts
rename to airflow/www/static/js/api/useMarkTaskDryRun.ts
index 4e69875ffb..8e872ed8ea 100644
--- a/airflow/www/static/js/api/useConfirmMarkTask.ts
+++ b/airflow/www/static/js/api/useMarkTaskDryRun.ts
@@ -18,41 +18,48 @@
*/
import axios, { AxiosResponse } from "axios";
-import { useMutation } from "react-query";
+import { useQuery } from "react-query";
import type { TaskState } from "src/types";
import URLSearchParamsWrapper from "src/utils/URLSearchParamWrapper";
import { getMetaValue } from "../utils";
-import useErrorToast from "../utils/useErrorToast";
const confirmUrl = getMetaValue("confirm_url");
-export default function useConfirmMarkTask({
+const useMarkTaskDryRun = ({
dagId,
runId,
taskId,
state,
+ past,
+ future,
+ upstream,
+ downstream,
+ mapIndexes = [],
}: {
dagId: string;
runId: string;
taskId: string;
state: TaskState;
-}) {
- const errorToast = useErrorToast();
- return useMutation(
- ["confirmStateChange", dagId, runId, taskId, state],
- ({
+ past: boolean;
+ future: boolean;
+ upstream: boolean;
+ downstream: boolean;
+ mapIndexes?: number[];
+}) =>
+ useQuery(
+ [
+ "confirmStateChange",
+ dagId,
+ runId,
+ taskId,
+ state,
past,
future,
upstream,
downstream,
- mapIndexes = [],
- }: {
- past: boolean;
- future: boolean;
- upstream: boolean;
- downstream: boolean;
- mapIndexes: number[];
- }) => {
+ mapIndexes,
+ ],
+ () => {
const params = new URLSearchParamsWrapper({
dag_id: dagId,
dag_run_id: runId,
@@ -68,9 +75,7 @@ export default function useConfirmMarkTask({
params.append("map_index", mi.toString());
});
return axios.get<AxiosResponse, string[]>(confirmUrl, { params });
- },
- {
- onError: (error: Error) => errorToast({ error }),
}
);
-}
+
+export default useMarkTaskDryRun;
diff --git a/airflow/www/static/js/components/ConfirmDialog.tsx b/airflow/www/static/js/components/ConfirmDialog.tsx
deleted file mode 100644
index 4826f11ff5..0000000000
--- a/airflow/www/static/js/components/ConfirmDialog.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-/*!
- * 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 React, { PropsWithChildren, useRef } from "react";
-import {
- AlertDialog,
- AlertDialogBody,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogContent,
- AlertDialogOverlay,
- Button,
- Code,
- Text,
-} from "@chakra-ui/react";
-
-import { useContainerRef } from "src/context/containerRef";
-
-interface Props extends PropsWithChildren {
- isOpen: boolean;
- onClose: () => void;
- title?: string;
- description: string;
- affectedTasks: string[];
- onConfirm: () => void;
- isLoading?: boolean;
-}
-
-const ConfirmDialog = ({
- isOpen,
- onClose,
- title = "Wait a minute",
- description,
- affectedTasks,
- onConfirm,
- isLoading = false,
- children,
-}: Props) => {
- const initialFocusRef = useRef<HTMLButtonElement>(null);
- const containerRef = useContainerRef();
-
- return (
- <AlertDialog
- isOpen={isOpen}
- // Since we are not deleting, we can focus on the confirm button
- leastDestructiveRef={initialFocusRef}
- onClose={onClose}
- portalProps={{ containerRef }}
- size="6xl"
- blockScrollOnMount={false}
- >
- <AlertDialogOverlay>
- <AlertDialogContent maxHeight="90vh">
- <AlertDialogHeader fontSize="4xl" fontWeight="bold">
- {title}
- </AlertDialogHeader>
-
- <AlertDialogBody overflowY="auto">
- {children}
- <Text mb={2}>{description}</Text>
- {affectedTasks.map((ti) => (
- <Code width="100%" key={ti} fontSize="lg">
- {ti}
- </Code>
- ))}
- {!affectedTasks.length && <Text>No task instances to change.</Text>}
- </AlertDialogBody>
-
- <AlertDialogFooter>
- <Button onClick={onClose}>Cancel</Button>
- <Button
- colorScheme="blue"
- onClick={onConfirm}
- ml={3}
- ref={initialFocusRef}
- isLoading={isLoading}
- >
- Confirm
- </Button>
- </AlertDialogFooter>
- </AlertDialogContent>
- </AlertDialogOverlay>
- </AlertDialog>
- );
-};
-
-export default ConfirmDialog;
diff --git a/airflow/www/static/js/dag/details/dagRun/ClearRun.tsx b/airflow/www/static/js/dag/details/dagRun/ClearRun.tsx
index d943bbeb13..dbc369799b 100644
--- a/airflow/www/static/js/dag/details/dagRun/ClearRun.tsx
+++ b/airflow/www/static/js/dag/details/dagRun/ClearRun.tsx
@@ -17,51 +17,68 @@
* under the License.
*/
-import React, { useState } from "react";
-import { Button, useDisclosure } from "@chakra-ui/react";
-
-import { useClearRun } from "src/api";
+import React from "react";
+import {
+ Flex,
+ Button,
+ Menu,
+ MenuButton,
+ MenuItem,
+ MenuList,
+ MenuButtonProps,
+} from "@chakra-ui/react";
+import { MdArrowDropDown } from "react-icons/md";
import { getMetaValue } from "src/utils";
-import ConfirmDialog from "src/components/ConfirmDialog";
+import { useClearRun, useQueueRun } from "src/api";
const canEdit = getMetaValue("can_edit") === "True";
+const dagId = getMetaValue("dag_id");
-interface Props {
- dagId: string;
+interface Props extends MenuButtonProps {
runId: string;
}
-const ClearRun = ({ dagId, runId }: Props) => {
- const [affectedTasks, setAffectedTasks] = useState<string[]>([]);
- const { isOpen, onOpen, onClose } = useDisclosure();
- const { mutateAsync: onClear, isLoading } = useClearRun(dagId, runId);
+const ClearRun = ({ runId, ...otherProps }: Props) => {
+ const { mutateAsync: onClear, isLoading: isClearLoading } = useClearRun(
+ dagId,
+ runId
+ );
+
+ const { mutateAsync: onQueue, isLoading: isQueueLoading } = useQueueRun(
+ dagId,
+ runId
+ );
- const onClick = async () => {
- const data = await onClear({ confirmed: false });
- setAffectedTasks(data);
- onOpen();
+ const clearExistingTasks = () => {
+ onClear({ confirmed: true });
};
- const onConfirm = async () => {
- await onClear({ confirmed: true });
- setAffectedTasks([]);
- onClose();
+ const queueNewTasks = () => {
+ onQueue({ confirmed: true });
};
+ const clearLabel = "Clear tasks or add new tasks";
return (
- <>
- <Button onClick={onClick} isLoading={isLoading} isDisabled={!canEdit}>
- Clear existing tasks
- </Button>
- <ConfirmDialog
- isOpen={isOpen}
- onClose={onClose}
- onConfirm={onConfirm}
- isLoading={isLoading}
- description="Task instances you are about to clear:"
- affectedTasks={affectedTasks}
- />
- </>
+ <Menu>
+ <MenuButton
+ as={Button}
+ colorScheme="blue"
+ transition="all 0.2s"
+ title={clearLabel}
+ aria-label={clearLabel}
+ disabled={!canEdit || isClearLoading || isQueueLoading}
+ {...otherProps}
+ >
+ <Flex>
+ Clear
+ <MdArrowDropDown size="16px" />
+ </Flex>
+ </MenuButton>
+ <MenuList>
+ <MenuItem onClick={clearExistingTasks}>Clear existing tasks</MenuItem>
+ <MenuItem onClick={queueNewTasks}>Queue up new tasks</MenuItem>
+ </MenuList>
+ </Menu>
);
};
diff --git a/airflow/www/static/js/dag/details/dagRun/MarkFailedRun.tsx b/airflow/www/static/js/dag/details/dagRun/MarkFailedRun.tsx
deleted file mode 100644
index cd403de69f..0000000000
--- a/airflow/www/static/js/dag/details/dagRun/MarkFailedRun.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-/*!
- * 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 React, { useState } from "react";
-import { Button, useDisclosure } from "@chakra-ui/react";
-
-import { useMarkFailedRun } from "src/api";
-import { getMetaValue } from "src/utils";
-import ConfirmDialog from "src/components/ConfirmDialog";
-
-const canEdit = getMetaValue("can_edit") === "True";
-
-interface Props {
- dagId: string;
- runId: string;
-}
-
-const MarkFailedRun = ({ dagId, runId }: Props) => {
- const [affectedTasks, setAffectedTasks] = useState<string[]>([]);
- const { isOpen, onOpen, onClose } = useDisclosure();
- const { mutateAsync: markFailed, isLoading } = useMarkFailedRun(dagId, runId);
-
- const onClick = async () => {
- const data = await markFailed({ confirmed: false });
- setAffectedTasks(data);
- onOpen();
- };
-
- const onConfirm = () => {
- markFailed({ confirmed: true });
- setAffectedTasks([]);
- onClose();
- };
-
- return (
- <>
- <Button
- onClick={onClick}
- colorScheme="red"
- isLoading={isLoading}
- isDisabled={!canEdit}
- >
- Mark Failed
- </Button>
- <ConfirmDialog
- isOpen={isOpen}
- onClose={onClose}
- onConfirm={onConfirm}
- isLoading={isLoading}
- description="Task instances you are about to mark as failed or skipped:"
- affectedTasks={affectedTasks}
- />
- </>
- );
-};
-
-export default MarkFailedRun;
diff --git a/airflow/www/static/js/dag/details/dagRun/MarkRunAs.tsx b/airflow/www/static/js/dag/details/dagRun/MarkRunAs.tsx
new file mode 100644
index 0000000000..276e9a6f05
--- /dev/null
+++ b/airflow/www/static/js/dag/details/dagRun/MarkRunAs.tsx
@@ -0,0 +1,90 @@
+/*!
+ * 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 React from "react";
+import {
+ Flex,
+ Button,
+ Menu,
+ MenuButton,
+ MenuItem,
+ MenuList,
+ MenuButtonProps,
+} from "@chakra-ui/react";
+import { MdArrowDropDown } from "react-icons/md";
+import { getMetaValue } from "src/utils";
+import { useMarkFailedRun, useMarkSuccessRun } from "src/api";
+import type { RunState } from "src/types";
+
+import { SimpleStatus } from "../../StatusBox";
+
+const canEdit = getMetaValue("can_edit") === "True";
+const dagId = getMetaValue("dag_id");
+
+interface Props extends MenuButtonProps {
+ runId: string;
+ state?: RunState;
+}
+
+const MarkRunAs = ({ runId, state, ...otherProps }: Props) => {
+ const { mutateAsync: markFailed, isLoading: isMarkFailedLoading } =
+ useMarkFailedRun(dagId, runId);
+ const { mutateAsync: markSuccess, isLoading: isMarkSuccessLoading } =
+ useMarkSuccessRun(dagId, runId);
+
+ const markAsFailed = () => {
+ markFailed({ confirmed: true });
+ };
+
+ const markAsSuccess = () => {
+ markSuccess({ confirmed: true });
+ };
+
+ const markLabel = "Manually set dag run state";
+ return (
+ <Menu>
+ <MenuButton
+ as={Button}
+ colorScheme="blue"
+ transition="all 0.2s"
+ title={markLabel}
+ aria-label={markLabel}
+ disabled={!canEdit || isMarkFailedLoading || isMarkSuccessLoading}
+ {...otherProps}
+ >
+ <Flex>
+ Mark state as...
+ <MdArrowDropDown size="16px" />
+ </Flex>
+ </MenuButton>
+ <MenuList>
+ <MenuItem onClick={markAsFailed} isDisabled={state === "failed"}>
+ <SimpleStatus state="failed" mr={2} />
+ failed
+ </MenuItem>
+ <MenuItem onClick={markAsSuccess} isDisabled={state === "success"}>
+ <SimpleStatus state="success" mr={2} />
+ success
+ </MenuItem>
+ </MenuList>
+ </Menu>
+ );
+};
+
+export default MarkRunAs;
diff --git a/airflow/www/static/js/dag/details/dagRun/MarkSuccessRun.tsx b/airflow/www/static/js/dag/details/dagRun/MarkSuccessRun.tsx
deleted file mode 100644
index 17b1f679b7..0000000000
--- a/airflow/www/static/js/dag/details/dagRun/MarkSuccessRun.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*!
- * 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 React, { useState } from "react";
-import { Button, useDisclosure } from "@chakra-ui/react";
-
-import { useMarkSuccessRun } from "src/api";
-import ConfirmDialog from "src/components/ConfirmDialog";
-import { getMetaValue } from "src/utils";
-
-const canEdit = getMetaValue("can_edit") === "True";
-
-interface Props {
- dagId: string;
- runId: string;
-}
-
-const MarkSuccessRun = ({ dagId, runId }: Props) => {
- const [affectedTasks, setAffectedTasks] = useState<string[]>([]);
- const { isOpen, onOpen, onClose } = useDisclosure();
- const { mutateAsync: markSuccess, isLoading } = useMarkSuccessRun(
- dagId,
- runId
- );
-
- const onClick = async () => {
- const data = await markSuccess({ confirmed: false });
- setAffectedTasks(data);
- onOpen();
- };
-
- const onConfirm = async () => {
- await markSuccess({ confirmed: true });
- setAffectedTasks([]);
- onClose();
- };
-
- return (
- <>
- <Button
- ml={2}
- onClick={onClick}
- colorScheme="green"
- isLoading={isLoading}
- isDisabled={!canEdit}
- >
- Mark Success
- </Button>
- <ConfirmDialog
- isOpen={isOpen}
- onClose={onClose}
- onConfirm={onConfirm}
- isLoading={isLoading}
- description="Task instances you are about to mark as success:"
- affectedTasks={affectedTasks}
- />
- </>
- );
-};
-
-export default MarkSuccessRun;
diff --git a/airflow/www/static/js/dag/details/dagRun/QueueRun.tsx b/airflow/www/static/js/dag/details/dagRun/QueueRun.tsx
deleted file mode 100644
index 8e8a054c40..0000000000
--- a/airflow/www/static/js/dag/details/dagRun/QueueRun.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-/*!
- * 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 React, { useState } from "react";
-import { Button, useDisclosure } from "@chakra-ui/react";
-
-import { useQueueRun } from "src/api";
-import ConfirmDialog from "src/components/ConfirmDialog";
-import { getMetaValue } from "src/utils";
-
-const canEdit = getMetaValue("can_edit") === "True";
-
-interface Props {
- dagId: string;
- runId: string;
-}
-
-const QueueRun = ({ dagId, runId }: Props) => {
- const [affectedTasks, setAffectedTasks] = useState<string[]>([]);
- const { isOpen, onOpen, onClose } = useDisclosure();
- const { mutateAsync: onQueue, isLoading } = useQueueRun(dagId, runId);
-
- // Get what the changes will be and show it in a modal
- const onClick = async () => {
- const data = await onQueue({ confirmed: false });
- setAffectedTasks(data);
- onOpen();
- };
-
- // Confirm changes
- const onConfirm = async () => {
- await onQueue({ confirmed: true });
- setAffectedTasks([]);
- onClose();
- };
-
- return (
- <>
- <Button
- onClick={onClick}
- isLoading={isLoading}
- ml={2}
- title="Queue up new tasks to make the DAG run up-to-date with any DAG file changes."
- isDisabled={!canEdit}
- >
- Queue up new tasks
- </Button>
- <ConfirmDialog
- isOpen={isOpen}
- onClose={onClose}
- onConfirm={onConfirm}
- isLoading={isLoading}
- description="Task instances you are about to queue:"
- affectedTasks={affectedTasks}
- />
- </>
- );
-};
-
-export default QueueRun;
diff --git a/airflow/www/static/js/dag/details/dagRun/index.tsx b/airflow/www/static/js/dag/details/dagRun/index.tsx
index 289da0dc64..ed9dce266f 100644
--- a/airflow/www/static/js/dag/details/dagRun/index.tsx
+++ b/airflow/www/static/js/dag/details/dagRun/index.tsx
@@ -19,7 +19,6 @@
import React, { useRef } from "react";
import {
Flex,
- Text,
Box,
Button,
Divider,
@@ -43,10 +42,6 @@ import Time from "src/components/Time";
import RunTypeIcon from "src/components/RunTypeIcon";
import NotesAccordion from "src/dag/details/NotesAccordion";
-import MarkFailedRun from "./MarkFailedRun";
-import MarkSuccessRun from "./MarkSuccessRun";
-import QueueRun from "./QueueRun";
-import ClearRun from "./ClearRun";
import DatasetTriggerEvents from "./DatasetTriggerEvents";
const dagId = getMetaValue("dag_id");
@@ -94,20 +89,6 @@ const DagRun = ({ runId }: Props) => {
overflowY="auto"
pb={4}
>
- <Flex justifyContent="space-between" alignItems="center">
- <Flex>
- <MarkFailedRun dagId={dagId} runId={runId} />
- <MarkSuccessRun dagId={dagId} runId={runId} />
- </Flex>
- <Flex justifyContent="flex-end" alignItems="center">
- <Text fontWeight="bold" mr={2}>
- Re-run:
- </Text>
- <ClearRun dagId={dagId} runId={runId} />
- <QueueRun dagId={dagId} runId={runId} />
- </Flex>
- </Flex>
- <Divider my={3} />
<Box px={4}>
<NotesAccordion
dagId={dagId}
diff --git a/airflow/www/static/js/dag/details/index.tsx b/airflow/www/static/js/dag/details/index.tsx
index 65cba38bbc..1cf9a55632 100644
--- a/airflow/www/static/js/dag/details/index.tsx
+++ b/airflow/www/static/js/dag/details/index.tsx
@@ -46,6 +46,10 @@ import MappedInstances from "./taskInstance/MappedInstances";
import Logs from "./taskInstance/Logs";
import BackToTaskSummary from "./taskInstance/BackToTaskSummary";
import FilterTasks from "./FilterTasks";
+import ClearRun from "./dagRun/ClearRun";
+import MarkRunAs from "./dagRun/MarkRunAs";
+import ClearInstance from "./taskInstance/taskActions/ClearInstance";
+import MarkInstanceAs from "./taskInstance/taskActions/MarkInstanceAs";
const dagId = getMetaValue("dag_id")!;
@@ -149,7 +153,38 @@ const Details = ({ openGroupIds, onToggleGroups, hoveredTaskState }: Props) => {
<Flex flexDirection="column" pl={3} height="100%">
<Flex alignItems="center" justifyContent="space-between">
<Header />
- <Flex>{taskId && runId && <FilterTasks taskId={taskId} />}</Flex>
+ <Flex>
+ {runId && !taskId && (
+ <>
+ <ClearRun runId={runId} mr={2} />
+ <MarkRunAs runId={runId} state={run?.state} />
+ </>
+ )}
+ {runId && taskId && (
+ <>
+ <ClearInstance
+ taskId={taskId}
+ runId={runId}
+ executionDate={run?.executionDate || ""}
+ isGroup={isGroup}
+ isMapped={isMapped}
+ mapIndex={mapIndex}
+ mr={2}
+ />
+ {!isGroup && (
+ <MarkInstanceAs
+ taskId={taskId}
+ runId={runId}
+ state={instance?.state}
+ isMapped={isMapped}
+ mapIndex={mapIndex}
+ mr={2}
+ />
+ )}
+ </>
+ )}
+ {taskId && runId && <FilterTasks taskId={taskId} />}
+ </Flex>
</Flex>
<Divider my={2} />
<Tabs
diff --git a/airflow/www/static/js/dag/details/taskInstance/index.tsx b/airflow/www/static/js/dag/details/taskInstance/index.tsx
index 7d584aa07c..e22e346610 100644
--- a/airflow/www/static/js/dag/details/taskInstance/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/index.tsx
@@ -28,7 +28,6 @@ import NotesAccordion from "src/dag/details/NotesAccordion";
import TaskNav from "./Nav";
import ExtraLinks from "./ExtraLinks";
import Details from "./Details";
-import TaskActions from "./taskActions";
const dagId = getMetaValue("dag_id")!;
@@ -42,7 +41,6 @@ const TaskInstance = ({ taskId, runId, mapIndex }: Props) => {
const taskInstanceRef = useRef<HTMLDivElement>(null);
const offsetTop = useOffsetTop(taskInstanceRef);
const isMapIndexDefined = !(mapIndex === undefined);
- const actionsMapIndexes = isMapIndexDefined ? [mapIndex] : [];
const {
data: { dagRuns, groups },
} = useGridData();
@@ -74,13 +72,6 @@ const TaskInstance = ({ taskId, runId, mapIndex }: Props) => {
const { executionDate } = run;
- let taskActionsTitle = `${isGroup ? "Task Group" : "Task"} Actions`;
- if (isMapped) {
- taskActionsTitle += ` for ${actionsMapIndexes.length || "all"} mapped task${
- actionsMapIndexes.length !== 1 ? "s" : ""
- }`;
- }
-
return (
<Box
py="4px"
@@ -108,17 +99,6 @@ const TaskInstance = ({ taskId, runId, mapIndex }: Props) => {
key={dagId + runId + taskId + instance.mapIndex}
/>
)}
- <Box mb={8}>
- <TaskActions
- title={taskActionsTitle}
- runId={runId}
- taskId={taskId}
- dagId={dagId}
- executionDate={executionDate}
- mapIndexes={actionsMapIndexes}
- isGroup={isGroup}
- />
- </Box>
{!isMapped && group.extraLinks && (
<ExtraLinks
taskId={taskId}
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionButton.tsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionButton.tsx
index 3e89fd44a4..7dcd406dda 100644
--- a/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionButton.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionButton.tsx
@@ -25,7 +25,7 @@ const titleMap = {
future: "Also include future task instances when clearing this one",
upstream: "Also include upstream dependencies",
downstream: "Also include downstream dependencies",
- recursive: "",
+ recursive: "Include subdags and parent dags",
failed: "Only consider failed task instances when clearing this one",
};
@@ -34,7 +34,7 @@ type KeysOfTitleMap = keyof typeof titleMap;
type Props = ButtonProps & { name: Capitalize<KeysOfTitleMap> };
const ActionButton = ({ name, ...rest }: Props) => (
<Button title={titleMap[name.toLowerCase() as KeysOfTitleMap]} {...rest}>
- {name}
+ {name === "Failed" ? "Only Failed" : name}
</Button>
);
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionModal.tsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionModal.tsx
new file mode 100644
index 0000000000..ef89681150
--- /dev/null
+++ b/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionModal.tsx
@@ -0,0 +1,112 @@
+/*!
+ * 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 React, { ReactNode } from "react";
+import {
+ Button,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ ModalProps,
+ Box,
+ Text,
+ Accordion,
+ AccordionButton,
+ AccordionPanel,
+ AccordionItem,
+ AccordionIcon,
+ Code,
+} from "@chakra-ui/react";
+
+import { useContainerRef } from "src/context/containerRef";
+
+interface Props extends ModalProps {
+ affectedTasks?: string[];
+ header: ReactNode | string;
+ subheader?: ReactNode | string;
+ submitButton: ReactNode;
+}
+
+const ActionModal = ({
+ isOpen,
+ onClose,
+ children,
+ header,
+ subheader,
+ affectedTasks = [],
+ submitButton,
+ ...otherProps
+}: Props) => {
+ const containerRef = useContainerRef();
+ return (
+ <Modal
+ size="6xl"
+ isOpen={isOpen}
+ onClose={onClose}
+ portalProps={{ containerRef }}
+ blockScrollOnMount={false}
+ {...otherProps}
+ >
+ <ModalOverlay />
+ <ModalContent>
+ <ModalHeader>{header}</ModalHeader>
+ <ModalCloseButton />
+ <ModalBody>
+ <Box mb={3}>{subheader}</Box>
+ <Box>
+ {children}
+ <Accordion allowToggle my={3}>
+ <AccordionItem>
+ <AccordionButton>
+ <Box flex="1" textAlign="left">
+ <Text as="strong" size="lg">
+ Affected Tasks: {affectedTasks?.length || 0}
+ </Text>
+ </Box>
+ <AccordionIcon />
+ </AccordionButton>
+ <AccordionPanel>
+ <Box maxHeight="400px" overflowY="auto">
+ {(affectedTasks || []).map((ti) => (
+ <Code width="100%" key={ti} fontSize="lg">
+ {ti}
+ </Code>
+ ))}
+ </Box>
+ </AccordionPanel>
+ </AccordionItem>
+ </Accordion>
+ </Box>
+ </ModalBody>
+ <ModalFooter justifyContent="space-between">
+ <Button colorScheme="gray" onClick={onClose}>
+ Cancel
+ </Button>
+ {submitButton}
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+ );
+};
+
+export default ActionModal;
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/Clear.tsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/Clear.tsx
deleted file mode 100644
index e42f419014..0000000000
--- a/airflow/www/static/js/dag/details/taskInstance/taskActions/Clear.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-/*!
- * 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 React, { useState } from "react";
-import {
- Button,
- Flex,
- ButtonGroup,
- useDisclosure,
- Alert,
- AlertIcon,
-} from "@chakra-ui/react";
-
-import ConfirmDialog from "src/components/ConfirmDialog";
-import { useClearTask } from "src/api";
-import { getMetaValue } from "src/utils";
-
-import ActionButton from "./ActionButton";
-import type { CommonActionProps } from "./types";
-
-const canEdit = getMetaValue("can_edit") === "True";
-
-const Run = ({
- dagId,
- runId,
- taskId,
- executionDate,
- mapIndexes,
- isGroup,
-}: CommonActionProps) => {
- const [affectedTasks, setAffectedTasks] = useState<string[]>([]);
-
- // Options check/unchecked
- const [past, setPast] = useState(false);
- const onTogglePast = () => setPast(!past);
-
- const [future, setFuture] = useState(false);
- const onToggleFuture = () => setFuture(!future);
-
- const [upstream, setUpstream] = useState(false);
- const onToggleUpstream = () => setUpstream(!upstream);
-
- const [downstream, setDownstream] = useState(true);
- const onToggleDownstream = () => setDownstream(!downstream);
-
- const [recursive, setRecursive] = useState(true);
- const onToggleRecursive = () => setRecursive(!recursive);
-
- const [failed, setFailed] = useState(false);
- const onToggleFailed = () => setFailed(!failed);
-
- // Confirm dialog open/close
- const { isOpen, onOpen, onClose } = useDisclosure();
-
- const { mutateAsync: clearTask, isLoading } = useClearTask({
- dagId,
- runId,
- taskId,
- executionDate,
- isGroup: !!isGroup,
- });
-
- const onClick = async () => {
- const data = await clearTask({
- past,
- future,
- upstream,
- downstream,
- recursive,
- failed,
- confirmed: false,
- mapIndexes,
- });
- setAffectedTasks(data);
- onOpen();
- };
-
- const onConfirm = async () => {
- await clearTask({
- past,
- future,
- upstream,
- downstream,
- recursive,
- failed,
- confirmed: true,
- mapIndexes,
- });
- setAffectedTasks([]);
- onClose();
- };
-
- return (
- <Flex justifyContent="space-between" width="100%">
- <ButtonGroup isAttached variant="outline" isDisabled={!canEdit}>
- <ActionButton
- bg={past ? "gray.100" : undefined}
- onClick={onTogglePast}
- name="Past"
- />
- <ActionButton
- bg={future ? "gray.100" : undefined}
- onClick={onToggleFuture}
- name="Future"
- />
- <ActionButton
- bg={upstream ? "gray.100" : undefined}
- onClick={onToggleUpstream}
- name="Upstream"
- />
- <ActionButton
- bg={downstream ? "gray.100" : undefined}
- onClick={onToggleDownstream}
- name="Downstream"
- />
- <ActionButton
- bg={recursive ? "gray.100" : undefined}
- onClick={onToggleRecursive}
- name="Recursive"
- />
- <ActionButton
- bg={failed ? "gray.100" : undefined}
- onClick={onToggleFailed}
- name="Failed"
- />
- </ButtonGroup>
- <Button
- colorScheme="blue"
- onClick={onClick}
- isLoading={isLoading}
- isDisabled={!canEdit}
- title="Clearing deletes the previous state of the task instance, allowing it to get re-triggered by the scheduler or a backfill command"
- >
- Clear
- </Button>
- <ConfirmDialog
- isOpen={isOpen}
- onClose={onClose}
- onConfirm={onConfirm}
- isLoading={isLoading}
- description={`Task instances you are about to clear (${affectedTasks.length}):`}
- affectedTasks={affectedTasks}
- >
- {isGroup && (past || future) && (
- <Alert status="warning" mb={3}>
- <AlertIcon />
- Clearing a TaskGroup in the future and/or past will affect all the
- tasks of this group across multiple dag runs.
- <br />
- This can take a while to complete.
- </Alert>
- )}
- </ConfirmDialog>
- </Flex>
- );
-};
-
-export default Run;
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/ClearInstance.tsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/ClearInstance.tsx
new file mode 100644
index 0000000000..d16856255f
--- /dev/null
+++ b/airflow/www/static/js/dag/details/taskInstance/taskActions/ClearInstance.tsx
@@ -0,0 +1,235 @@
+/*!
+ * 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 React, { useState } from "react";
+import {
+ Alert,
+ AlertIcon,
+ Box,
+ Button,
+ ButtonGroup,
+ ButtonProps,
+ Text,
+ useDisclosure,
+} from "@chakra-ui/react";
+
+import { getMetaValue } from "src/utils";
+import { useClearTask } from "src/api";
+import useClearTaskDryRun from "src/api/useClearTaskDryRun";
+
+import ActionButton from "./ActionButton";
+import ActionModal from "./ActionModal";
+
+const canEdit = getMetaValue("can_edit") === "True";
+const dagId = getMetaValue("dag_id");
+
+interface Props extends ButtonProps {
+ runId: string;
+ taskId: string;
+ executionDate: string;
+ isGroup?: boolean;
+ isMapped?: boolean;
+ mapIndex?: number;
+}
+
+const ClearInstance = ({
+ runId,
+ taskId,
+ mapIndex,
+ executionDate,
+ isGroup,
+ isMapped,
+ ...otherProps
+}: Props) => {
+ const { onOpen, onClose, isOpen } = useDisclosure();
+
+ const [past, setPast] = useState(false);
+ const onTogglePast = () => setPast(!past);
+
+ const [future, setFuture] = useState(false);
+ const onToggleFuture = () => setFuture(!future);
+
+ const [upstream, setUpstream] = useState(false);
+ const onToggleUpstream = () => setUpstream(!upstream);
+
+ const [downstream, setDownstream] = useState(false);
+ const onToggleDownstream = () => setDownstream(!downstream);
+
+ const [recursive, setRecursive] = useState(true);
+ const onToggleRecursive = () => setRecursive(!recursive);
+
+ const [failed, setFailed] = useState(false);
+ const onToggleFailed = () => setFailed(!failed);
+
+ const mapIndexes =
+ mapIndex !== undefined && mapIndex !== -1 ? [mapIndex] : undefined;
+
+ const { data: affectedTasks, isLoading: isLoadingDryRun } =
+ useClearTaskDryRun({
+ dagId,
+ runId,
+ taskId,
+ executionDate,
+ isGroup: !!isGroup,
+ past,
+ future,
+ upstream,
+ downstream,
+ recursive,
+ failed,
+ mapIndexes,
+ });
+
+ const { mutateAsync: clearTask, isLoading } = useClearTask({
+ dagId,
+ runId,
+ taskId,
+ executionDate,
+ isGroup: !!isGroup,
+ });
+
+ const resetModal = () => {
+ onClose();
+ setDownstream(false);
+ setUpstream(false);
+ setPast(false);
+ setFuture(false);
+ setRecursive(false);
+ setFailed(false);
+ };
+
+ const onClear = () => {
+ clearTask({
+ confirmed: true,
+ past,
+ future,
+ upstream,
+ downstream,
+ recursive,
+ failed,
+ mapIndexes,
+ });
+ resetModal();
+ };
+
+ const clearLabel = "Clear and retry task.";
+
+ return (
+ <>
+ <Button
+ title={clearLabel}
+ aria-label={clearLabel}
+ ml={2}
+ isDisabled={!canEdit}
+ colorScheme="blue"
+ onClick={onOpen}
+ {...otherProps}
+ >
+ Clear task
+ </Button>
+ <ActionModal
+ isOpen={isOpen}
+ onClose={resetModal}
+ header="Clear and Retry"
+ subheader={
+ <>
+ <Text>
+ <Text as="strong" mr={1}>
+ Task:
+ </Text>
+ {taskId}
+ </Text>
+ <Text>
+ <Text as="strong" mr={1}>
+ Run:
+ </Text>
+ {runId}
+ </Text>
+ {isMapped && (
+ <Text>
+ <Text as="strong" mr={1}>
+ Map Index:
+ </Text>
+ {mapIndex !== undefined ? mapIndex : `All mapped tasks`}
+ </Text>
+ )}
+ </>
+ }
+ affectedTasks={affectedTasks}
+ submitButton={
+ <Button
+ colorScheme="blue"
+ isLoading={isLoading || isLoadingDryRun}
+ isDisabled={!affectedTasks?.length}
+ onClick={onClear}
+ >
+ Clear
+ </Button>
+ }
+ >
+ <Box>
+ <Text>Include: </Text>
+ <ButtonGroup isAttached variant="outline" isDisabled={!canEdit}>
+ <ActionButton
+ bg={past ? "gray.100" : undefined}
+ onClick={onTogglePast}
+ name="Past"
+ />
+ <ActionButton
+ bg={future ? "gray.100" : undefined}
+ onClick={onToggleFuture}
+ name="Future"
+ />
+ <ActionButton
+ bg={upstream ? "gray.100" : undefined}
+ onClick={onToggleUpstream}
+ name="Upstream"
+ />
+ <ActionButton
+ bg={downstream ? "gray.100" : undefined}
+ onClick={onToggleDownstream}
+ name="Downstream"
+ />
+ <ActionButton
+ bg={recursive ? "gray.100" : undefined}
+ onClick={onToggleRecursive}
+ name="Recursive"
+ />
+ <ActionButton
+ bg={failed ? "gray.100" : undefined}
+ onClick={onToggleFailed}
+ name="Failed"
+ />
+ </ButtonGroup>
+ </Box>
+ {isGroup && (past || future) && (
+ <Alert status="warning" my={3}>
+ <AlertIcon />
+ Clearing a TaskGroup in the future and/or past will affect all the
+ tasks of this group across multiple dag runs.
+ <br />
+ This can take a while to complete.
+ </Alert>
+ )}
+ </ActionModal>
+ </>
+ );
+};
+
+export default ClearInstance;
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/MarkFailed.tsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/MarkFailed.tsx
deleted file mode 100644
index 730cadc48d..0000000000
--- a/airflow/www/static/js/dag/details/taskInstance/taskActions/MarkFailed.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-/*!
- * 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 React, { useState } from "react";
-import { Button, Flex, ButtonGroup, useDisclosure } from "@chakra-ui/react";
-
-import { useConfirmMarkTask, useMarkFailedTask } from "src/api";
-import ConfirmDialog from "src/components/ConfirmDialog";
-import { getMetaValue } from "src/utils";
-
-import ActionButton from "./ActionButton";
-
-const canEdit = getMetaValue("can_edit") === "True";
-
-interface Props {
- dagId: string;
- runId: string;
- taskId: string;
- mapIndexes: number[];
-}
-
-const MarkFailed = ({ dagId, runId, taskId, mapIndexes }: Props) => {
- const [affectedTasks, setAffectedTasks] = useState<string[]>([]);
-
- // Options check/unchecked
- const [past, setPast] = useState(false);
- const onTogglePast = () => setPast(!past);
-
- const [future, setFuture] = useState(false);
- const onToggleFuture = () => setFuture(!future);
-
- const [upstream, setUpstream] = useState(false);
- const onToggleUpstream = () => setUpstream(!upstream);
-
- const [downstream, setDownstream] = useState(false);
- const onToggleDownstream = () => setDownstream(!downstream);
-
- // Confirm dialog open/close
- const { isOpen, onOpen, onClose } = useDisclosure();
-
- const { mutateAsync: markFailedMutation, isLoading: isMarkLoading } =
- useMarkFailedTask({
- dagId,
- runId,
- taskId,
- });
- const { mutateAsync: confirmChangeMutation, isLoading: isConfirmLoading } =
- useConfirmMarkTask({
- dagId,
- runId,
- taskId,
- state: "failed",
- });
-
- const onClick = async () => {
- const data = await confirmChangeMutation({
- past,
- future,
- upstream,
- downstream,
- mapIndexes,
- });
- setAffectedTasks(data);
- onOpen();
- };
-
- const onConfirm = async () => {
- await markFailedMutation({
- past,
- future,
- upstream,
- downstream,
- mapIndexes,
- });
- setAffectedTasks([]);
- onClose();
- };
-
- const isLoading = isMarkLoading || isConfirmLoading;
-
- return (
- <Flex justifyContent="space-between" width="100%">
- <ButtonGroup isAttached variant="outline" isDisabled={!canEdit}>
- <ActionButton
- bg={past ? "gray.100" : undefined}
- onClick={onTogglePast}
- name="Past"
- />
- <ActionButton
- bg={future ? "gray.100" : undefined}
- onClick={onToggleFuture}
- name="Future"
- />
- <ActionButton
- bg={upstream ? "gray.100" : undefined}
- onClick={onToggleUpstream}
- name="Upstream"
- />
- <ActionButton
- bg={downstream ? "gray.100" : undefined}
- onClick={onToggleDownstream}
- name="Downstream"
- />
- </ButtonGroup>
- <Button
- colorScheme="red"
- onClick={onClick}
- isLoading={isLoading}
- isDisabled={!canEdit}
- >
- Mark Failed
- </Button>
- <ConfirmDialog
- isOpen={isOpen}
- onClose={onClose}
- onConfirm={onConfirm}
- isLoading={isLoading}
- description="Task instances you are about to mark as failed:"
- affectedTasks={affectedTasks}
- />
- </Flex>
- );
-};
-
-export default MarkFailed;
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/MarkInstanceAs.tsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/MarkInstanceAs.tsx
new file mode 100644
index 0000000000..600aaeb64e
--- /dev/null
+++ b/airflow/www/static/js/dag/details/taskInstance/taskActions/MarkInstanceAs.tsx
@@ -0,0 +1,267 @@
+/*!
+ * 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 React, { useState } from "react";
+import {
+ Flex,
+ Button,
+ Menu,
+ MenuButton,
+ MenuItem,
+ MenuList,
+ MenuButtonProps,
+ useDisclosure,
+ ButtonGroup,
+ Box,
+ Text,
+} from "@chakra-ui/react";
+import { MdArrowDropDown } from "react-icons/md";
+import { capitalize } from "lodash";
+
+import { getMetaValue } from "src/utils";
+import type { TaskState } from "src/types";
+import {
+ useMarkFailedTask,
+ useMarkSuccessTask,
+ useMarkTaskDryRun,
+} from "src/api";
+
+import { SimpleStatus } from "../../../StatusBox";
+import ActionButton from "./ActionButton";
+import ActionModal from "./ActionModal";
+
+const canEdit = getMetaValue("can_edit") === "True";
+const dagId = getMetaValue("dag_id");
+
+interface Props extends MenuButtonProps {
+ runId: string;
+ taskId: string;
+ state?: TaskState;
+ mapIndex?: number;
+ isMapped?: boolean;
+}
+
+const MarkInstanceAs = ({
+ runId,
+ taskId,
+ mapIndex,
+ isMapped,
+ state: currentState,
+ ...otherProps
+}: Props) => {
+ const { onOpen, onClose, isOpen } = useDisclosure();
+
+ const [newState, setNewState] = useState<"failed" | "success">("success");
+
+ const [past, setPast] = useState(false);
+ const onTogglePast = () => setPast(!past);
+
+ const [future, setFuture] = useState(false);
+ const onToggleFuture = () => setFuture(!future);
+
+ const [upstream, setUpstream] = useState(false);
+ const onToggleUpstream = () => setUpstream(!upstream);
+
+ const [downstream, setDownstream] = useState(false);
+ const onToggleDownstream = () => setDownstream(!downstream);
+
+ const markAsFailed = () => {
+ setNewState("failed");
+ onOpen();
+ };
+
+ const markAsSuccess = () => {
+ setNewState("success");
+ onOpen();
+ };
+
+ const mapIndexes =
+ mapIndex !== undefined && mapIndex !== -1 ? [mapIndex] : undefined;
+
+ const { data: affectedTasks, isLoading: isLoadingDryRun } = useMarkTaskDryRun(
+ {
+ dagId,
+ runId,
+ taskId,
+ state: newState,
+ past,
+ future,
+ upstream,
+ downstream,
+ mapIndexes,
+ }
+ );
+
+ const { mutateAsync: markFailedMutation, isLoading: isMarkFailedLoading } =
+ useMarkFailedTask({
+ dagId,
+ runId,
+ taskId,
+ });
+
+ const { mutateAsync: markSuccessMutation, isLoading: isMarkSuccessLoading } =
+ useMarkSuccessTask({
+ dagId,
+ runId,
+ taskId,
+ });
+
+ const resetModal = () => {
+ onClose();
+ setDownstream(false);
+ setUpstream(false);
+ setPast(false);
+ setFuture(false);
+ };
+
+ const onMarkState = () => {
+ if (newState === "success") {
+ markSuccessMutation({
+ past,
+ future,
+ upstream,
+ downstream,
+ mapIndexes,
+ });
+ } else if (newState === "failed") {
+ markFailedMutation({
+ past,
+ future,
+ upstream,
+ downstream,
+ mapIndexes,
+ });
+ }
+ resetModal();
+ };
+
+ const markLabel = "Manually set task instance state";
+ const isMappedSummary = isMapped && mapIndex === undefined;
+
+ return (
+ <>
+ <Menu>
+ <MenuButton
+ as={Button}
+ colorScheme="blue"
+ transition="all 0.2s"
+ title={markLabel}
+ aria-label={markLabel}
+ disabled={!canEdit}
+ {...otherProps}
+ >
+ <Flex>
+ Mark state as…
+ <MdArrowDropDown size="16px" />
+ </Flex>
+ </MenuButton>
+ <MenuList>
+ <MenuItem
+ onClick={markAsFailed}
+ isDisabled={!isMappedSummary && currentState === "failed"}
+ >
+ <SimpleStatus state="failed" mr={2} />
+ failed
+ </MenuItem>
+ <MenuItem
+ onClick={markAsSuccess}
+ isDisabled={!isMappedSummary && currentState === "success"}
+ >
+ <SimpleStatus state="success" mr={2} />
+ success
+ </MenuItem>
+ </MenuList>
+ </Menu>
+ <ActionModal
+ isOpen={isOpen}
+ onClose={resetModal}
+ header={`Mark as ${capitalize(newState)}`}
+ subheader={
+ <>
+ <Text>
+ <Text as="strong" mr={1}>
+ Task:
+ </Text>
+ {taskId}
+ </Text>
+ <Text>
+ <Text as="strong" mr={1}>
+ Run:
+ </Text>
+ {runId}
+ </Text>
+ {isMapped && (
+ <Text>
+ <Text as="strong" mr={1}>
+ Map Index:
+ </Text>
+ {mapIndex !== undefined ? mapIndex : `All mapped tasks`}
+ </Text>
+ )}
+ </>
+ }
+ affectedTasks={affectedTasks}
+ submitButton={
+ <Button
+ colorScheme={
+ (newState === "success" && "green") ||
+ (newState === "failed" && "red") ||
+ "grey"
+ }
+ isLoading={
+ isLoadingDryRun || isMarkSuccessLoading || isMarkFailedLoading
+ }
+ isDisabled={!affectedTasks?.length || !newState}
+ onClick={onMarkState}
+ >
+ Mark as {newState}
+ </Button>
+ }
+ >
+ <Box>
+ <Text>Include: </Text>
+ <ButtonGroup isAttached variant="outline" isDisabled={!canEdit}>
+ <ActionButton
+ bg={past ? "gray.100" : undefined}
+ onClick={onTogglePast}
+ name="Past"
+ />
+ <ActionButton
+ bg={future ? "gray.100" : undefined}
+ onClick={onToggleFuture}
+ name="Future"
+ />
+ <ActionButton
+ bg={upstream ? "gray.100" : undefined}
+ onClick={onToggleUpstream}
+ name="Upstream"
+ />
+ <ActionButton
+ bg={downstream ? "gray.100" : undefined}
+ onClick={onToggleDownstream}
+ name="Downstream"
+ />
+ </ButtonGroup>
+ </Box>
+ </ActionModal>
+ </>
+ );
+};
+
+export default MarkInstanceAs;
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/MarkSuccess.tsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/MarkSuccess.tsx
deleted file mode 100644
index 7b1b9af7ba..0000000000
--- a/airflow/www/static/js/dag/details/taskInstance/taskActions/MarkSuccess.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-/*!
- * 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 React, { useState } from "react";
-import { Button, Flex, ButtonGroup, useDisclosure } from "@chakra-ui/react";
-
-import ConfirmDialog from "src/components/ConfirmDialog";
-import { useMarkSuccessTask, useConfirmMarkTask } from "src/api";
-import { getMetaValue } from "src/utils";
-
-import ActionButton from "./ActionButton";
-
-const canEdit = getMetaValue("can_edit") === "True";
-
-interface Props {
- dagId: string;
- runId: string;
- taskId: string;
- mapIndexes: number[];
-}
-
-const MarkSuccess = ({ dagId, runId, taskId, mapIndexes }: Props) => {
- const [affectedTasks, setAffectedTasks] = useState<string[]>([]);
-
- // Options check/unchecked
- const [past, setPast] = useState(false);
- const onTogglePast = () => setPast(!past);
-
- const [future, setFuture] = useState(false);
- const onToggleFuture = () => setFuture(!future);
-
- const [upstream, setUpstream] = useState(false);
- const onToggleUpstream = () => setUpstream(!upstream);
-
- const [downstream, setDownstream] = useState(false);
- const onToggleDownstream = () => setDownstream(!downstream);
-
- // Confirm dialog open/close
- const { isOpen, onOpen, onClose } = useDisclosure();
-
- const { mutateAsync: markSuccessMutation, isLoading: isMarkLoading } =
- useMarkSuccessTask({
- dagId,
- runId,
- taskId,
- });
- const { mutateAsync: confirmChangeMutation, isLoading: isConfirmLoading } =
- useConfirmMarkTask({
- dagId,
- runId,
- taskId,
- state: "success",
- });
-
- const onClick = async () => {
- const data = await confirmChangeMutation({
- past,
- future,
- upstream,
- downstream,
- mapIndexes,
- });
- setAffectedTasks(data);
- onOpen();
- };
-
- const onConfirm = async () => {
- await markSuccessMutation({
- past,
- future,
- upstream,
- downstream,
- mapIndexes,
- });
- setAffectedTasks([]);
- onClose();
- };
-
- const isLoading = isMarkLoading || isConfirmLoading;
-
- return (
- <Flex justifyContent="space-between" width="100%">
- <ButtonGroup isAttached variant="outline" isDisabled={!canEdit}>
- <ActionButton
- bg={past ? "gray.100" : undefined}
- onClick={onTogglePast}
- name="Past"
- />
- <ActionButton
- bg={future ? "gray.100" : undefined}
- onClick={onToggleFuture}
- name="Future"
- />
- <ActionButton
- bg={upstream ? "gray.100" : undefined}
- onClick={onToggleUpstream}
- name="Upstream"
- />
- <ActionButton
- bg={downstream ? "gray.100" : undefined}
- onClick={onToggleDownstream}
- name="Downstream"
- />
- </ButtonGroup>
- <Button
- colorScheme="green"
- onClick={onClick}
- isLoading={isLoading}
- isDisabled={!canEdit}
- >
- Mark Success
- </Button>
- <ConfirmDialog
- isOpen={isOpen}
- onClose={onClose}
- onConfirm={onConfirm}
- isLoading={isLoading}
- description="Task instances you are about to mark as success:"
- affectedTasks={affectedTasks}
- />
- </Flex>
- );
-};
-
-export default MarkSuccess;
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/index.tsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/index.tsx
deleted file mode 100644
index 92e471bc26..0000000000
--- a/airflow/www/static/js/dag/details/taskInstance/taskActions/index.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-/*!
- * 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 React from "react";
-import { Box, VStack, Divider, StackDivider, Text } from "@chakra-ui/react";
-
-import type { CommonActionProps } from "./types";
-import ClearAction from "./Clear";
-import MarkFailedAction from "./MarkFailed";
-import MarkSuccessAction from "./MarkSuccess";
-
-type Props = {
- title: string;
-} & CommonActionProps;
-
-const TaskActions = ({
- title,
- runId,
- taskId,
- dagId,
- executionDate,
- mapIndexes,
- isGroup,
-}: Props) => (
- <Box my={3}>
- <Text as="strong" size="lg">
- {title}
- </Text>
- <Divider my={2} />
- {/* For now only ClearAction is supported for groups */}
- {isGroup ? (
- <ClearAction
- runId={runId}
- taskId={taskId}
- dagId={dagId}
- executionDate={executionDate}
- mapIndexes={mapIndexes}
- isGroup={isGroup}
- />
- ) : (
- <VStack justifyContent="center" divider={<StackDivider my={3} />}>
- <ClearAction
- runId={runId}
- taskId={taskId}
- dagId={dagId}
- executionDate={executionDate}
- mapIndexes={mapIndexes}
- isGroup={!!isGroup}
- />
- <MarkFailedAction
- runId={runId}
- taskId={taskId}
- dagId={dagId}
- mapIndexes={mapIndexes}
- />
- <MarkSuccessAction
- runId={runId}
- taskId={taskId}
- dagId={dagId}
- mapIndexes={mapIndexes}
- />
- </VStack>
- )}
- <Divider my={2} />
- </Box>
-);
-
-export default TaskActions;
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/types.ts b/airflow/www/static/js/dag/details/taskInstance/taskActions/types.ts
deleted file mode 100644
index 2ba75d1ed8..0000000000
--- a/airflow/www/static/js/dag/details/taskInstance/taskActions/types.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*!
- * 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 type { Dag, DagRun, TaskInstance } from "src/types";
-
-export interface CommonActionProps {
- runId: DagRun["runId"];
- taskId: TaskInstance["taskId"];
- dagId: Dag["id"];
- executionDate: DagRun["executionDate"];
- mapIndexes: number[];
- isGroup?: boolean;
-}