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 <[email protected]> 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; -}
