diff --git a/airflow/www/static/js/dag/details/dagRun/ClearRun.tsx b/airflow/www/static/js/dag/details/dagRun/ClearRun.tsx index 3c848191c9fa..827020f53791 100644 --- a/airflow/www/static/js/dag/details/dagRun/ClearRun.tsx +++ b/airflow/www/static/js/dag/details/dagRun/ClearRun.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React from "react"; +import React, { useState } from "react"; import { Flex, Button, @@ -32,6 +32,7 @@ import { getMetaValue } from "src/utils"; import { useKeysPress } from "src/utils/useKeysPress"; import keyboardShortcutIdentifier from "src/dag/keyboardShortcutIdentifier"; import { useClearRun, useQueueRun } from "src/api"; +import ConfirmationModal from "./ConfirmationModal"; const canEdit = getMetaValue("can_edit") === "True"; const dagId = getMetaValue("dag_id"); @@ -59,31 +60,67 @@ const ClearRun = ({ runId, ...otherProps }: Props) => { onQueue({ confirmed: true }); }; - useKeysPress(keyboardShortcutIdentifier.dagRunClear, clearExistingTasks); + const [showConfirmationModal, setShowConfirmationModal] = useState(false); + + const storedValue = localStorage.getItem("doNotShowClearRunModal"); + const [doNotShowAgain, setDoNotShowAgain] = useState( + storedValue ? JSON.parse(storedValue) : false + ); + + const confirmAction = () => { + localStorage.setItem( + "doNotShowClearRunModal", + JSON.stringify(doNotShowAgain) + ); + clearExistingTasks(); + setShowConfirmationModal(false); + }; + + useKeysPress(keyboardShortcutIdentifier.dagRunClear, () => { + if (!doNotShowAgain) { + setShowConfirmationModal(true); + } else clearExistingTasks(); + }); const clearLabel = "Clear tasks or add new tasks"; return ( - - + + + + Clear + + + + + Clear existing tasks + Queue up new tasks + + + setShowConfirmationModal(false)} + header="Confirmation" + submitButton={ + + } + doNotShowAgain={doNotShowAgain} + onDoNotShowAgainChange={(value) => setDoNotShowAgain(value)} > - - Clear - - - - - Clear existing tasks - Queue up new tasks - - + This DAG run will be cleared. Are you sure you want to proceed? + + ); }; diff --git a/airflow/www/static/js/dag/details/dagRun/ConfirmationModal.tsx b/airflow/www/static/js/dag/details/dagRun/ConfirmationModal.tsx new file mode 100644 index 000000000000..017ce1c2100a --- /dev/null +++ b/airflow/www/static/js/dag/details/dagRun/ConfirmationModal.tsx @@ -0,0 +1,99 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { ReactNode, useRef, cloneElement, ReactElement } from "react"; +import { + Button, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + ModalProps, + Box, + Checkbox, +} from "@chakra-ui/react"; + +import { useContainerRef } from "src/context/containerRef"; + +interface Props extends ModalProps { + header: ReactNode | string; + children: ReactNode | string; + submitButton: ReactElement; + doNotShowAgain: boolean; + onDoNotShowAgainChange?: (value: boolean) => void; +} + +const ConfirmationModal = ({ + isOpen, + onClose, + header, + children, + submitButton, + doNotShowAgain, + onDoNotShowAgainChange, + ...otherProps +}: Props) => { + const containerRef = useContainerRef(); + const submitButtonFocusRef = useRef(null); + + const handleClose = () => { + onClose(); + }; + + return ( + + + + {header} + + + {children} + + onDoNotShowAgainChange && onDoNotShowAgainChange(!doNotShowAgain) + } + > + Do not show this again. + + + + + {cloneElement(submitButton, { ref: submitButtonFocusRef })} + + + + ); +}; + +export default ConfirmationModal; diff --git a/airflow/www/static/js/dag/details/dagRun/MarkRunAs.tsx b/airflow/www/static/js/dag/details/dagRun/MarkRunAs.tsx index 36a145ba26d2..43c1107d8a05 100644 --- a/airflow/www/static/js/dag/details/dagRun/MarkRunAs.tsx +++ b/airflow/www/static/js/dag/details/dagRun/MarkRunAs.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React from "react"; +import React, { useState, useReducer } from "react"; import { Flex, Button, @@ -35,6 +35,7 @@ import { useMarkFailedRun, useMarkSuccessRun } from "src/api"; import type { RunState } from "src/types"; import { SimpleStatus } from "../../StatusBox"; +import ConfirmationModal from "./ConfirmationModal"; const canEdit = getMetaValue("can_edit") === "True"; const dagId = getMetaValue("dag_id"); @@ -44,12 +45,48 @@ interface Props extends MenuButtonProps { state?: RunState; } +interface State { + showConfirmationModal: boolean; + confirmingAction: "success" | "failed" | null; +} + +type Action = + | { type: "SHOW_CONFIRMATION_MODAL"; payload: "success" | "failed" } + | { type: "HIDE_CONFIRMATION_MODAL" }; + +const initialState = { + showConfirmationModal: false, + confirmingAction: null, +}; + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "SHOW_CONFIRMATION_MODAL": + return { + ...state, + showConfirmationModal: true, + confirmingAction: action.payload, + }; + case "HIDE_CONFIRMATION_MODAL": + return { ...state, showConfirmationModal: false, confirmingAction: null }; + default: + return state; + } +}; + const MarkRunAs = ({ runId, state, ...otherProps }: Props) => { const { mutateAsync: markFailed, isLoading: isMarkFailedLoading } = useMarkFailedRun(dagId, runId); const { mutateAsync: markSuccess, isLoading: isMarkSuccessLoading } = useMarkSuccessRun(dagId, runId); + const [stateReducer, dispatch] = useReducer(reducer, initialState); + + const storedValue = localStorage.getItem("doNotShowMarkRunModal"); + const [doNotShowAgain, setDoNotShowAgain] = useState( + storedValue ? JSON.parse(storedValue) : false + ); + const markAsFailed = () => { markFailed({ confirmed: true }); }; @@ -58,42 +95,87 @@ const MarkRunAs = ({ runId, state, ...otherProps }: Props) => { markSuccess({ confirmed: true }); }; + const confirmAction = () => { + localStorage.setItem( + "doNotShowMarkRunModal", + JSON.stringify(doNotShowAgain) + ); + if (stateReducer.confirmingAction === "failed") { + markAsFailed(); + } else if (stateReducer.confirmingAction === "success") { + markAsSuccess(); + } + dispatch({ type: "HIDE_CONFIRMATION_MODAL" }); + }; + useKeysPress(keyboardShortcutIdentifier.dagMarkSuccess, () => { - if (state !== "success") markAsSuccess(); + if (state !== "success") { + if (!doNotShowAgain) { + dispatch({ type: "SHOW_CONFIRMATION_MODAL", payload: "success" }); + } else markAsSuccess(); + } }); useKeysPress(keyboardShortcutIdentifier.dagMarkFailed, () => { - if (state !== "failed") markAsFailed(); + if (state !== "failed") { + if (!doNotShowAgain) { + dispatch({ type: "SHOW_CONFIRMATION_MODAL", payload: "failed" }); + } else markAsFailed(); + } }); const markLabel = "Manually set dag run state"; return ( - - + + + + Mark state as... + + + + + + + failed + + + + success + + + + dispatch({ type: "HIDE_CONFIRMATION_MODAL" })} + header="Confirmation" + submitButton={ + + } + doNotShowAgain={doNotShowAgain} + onDoNotShowAgainChange={(value) => setDoNotShowAgain(value)} > - - Mark state as... - - - - - - - failed - - - - success - - - + Are you sure you want to mark the DAG run as{" "} + {stateReducer.confirmingAction}? + + ); };