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 (
-
+ 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...
+
+
+
+
+
+
+
+
+ dispatch({ type: "HIDE_CONFIRMATION_MODAL" })}
+ header="Confirmation"
+ submitButton={
+
+ }
+ doNotShowAgain={doNotShowAgain}
+ onDoNotShowAgainChange={(value) => setDoNotShowAgain(value)}
>
-
- Mark state as...
-
-
-
-
-
-
-
-
+ Are you sure you want to mark the DAG run as{" "}
+ {stateReducer.confirmingAction}?
+
+ >
);
};