This is an automated email from the ASF dual-hosted git repository.

bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 002d15a9f1 confirmation dialog box for DAG run actions (#35393)
002d15a9f1 is described below

commit 002d15a9f15c7ba3ba5cf5c431afc32bff7ab4e7
Author: Aadya <[email protected]>
AuthorDate: Mon Feb 12 22:51:38 2024 +0530

    confirmation dialog box for DAG run actions (#35393)
    
    * added confirmation to MarkRunAs.tsx
    
    * added confirmation to ClearRun.tsx
    
    * Create ActionModal.tsx
    
    * edit confirmation line
    
    * delete unnecessary if condition ActionModal.tsx
    
    * add initialFocusRef to ActionModal.tsx
    
    * store doNotShowAgain in localstorage in ClearRun.tsx
    
    * store doNotShowAgain in localStorage in MarkRunAs.tsx
    
    * changed ActionModal to ConfirmationModal
    
    * changed ActionModal to ConfirmationModal
    
    * removed useEffect and doNotShowAgain
    
    * added useReducer in MarkRunAs.tsx
---
 .../www/static/js/dag/details/dagRun/ClearRun.tsx  |  81 ++++++++----
 .../js/dag/details/dagRun/ConfirmationModal.tsx    |  99 +++++++++++++++
 .../www/static/js/dag/details/dagRun/MarkRunAs.tsx | 140 ++++++++++++++++-----
 3 files changed, 269 insertions(+), 51 deletions(-)

diff --git a/airflow/www/static/js/dag/details/dagRun/ClearRun.tsx 
b/airflow/www/static/js/dag/details/dagRun/ClearRun.tsx
index 3c848191c9..827020f537 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 (
-    <Menu>
-      <MenuButton
-        as={Button}
-        colorScheme="blue"
-        transition="all 0.2s"
-        title={clearLabel}
-        aria-label={clearLabel}
-        disabled={!canEdit || isClearLoading || isQueueLoading}
-        {...otherProps}
-        mt={2}
+    <>
+      <Menu>
+        <MenuButton
+          as={Button}
+          colorScheme="blue"
+          transition="all 0.2s"
+          title={clearLabel}
+          aria-label={clearLabel}
+          disabled={!canEdit || isClearLoading || isQueueLoading}
+          {...otherProps}
+          mt={2}
+        >
+          <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>
+      <ConfirmationModal
+        isOpen={showConfirmationModal}
+        onClose={() => setShowConfirmationModal(false)}
+        header="Confirmation"
+        submitButton={
+          <Button onClick={confirmAction} colorScheme="blue">
+            Clear DAG run
+          </Button>
+        }
+        doNotShowAgain={doNotShowAgain}
+        onDoNotShowAgainChange={(value) => setDoNotShowAgain(value)}
       >
-        <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>
+        This DAG run will be cleared. Are you sure you want to proceed?
+      </ConfirmationModal>
+    </>
   );
 };
 
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 0000000000..017ce1c210
--- /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<HTMLButtonElement>(null);
+
+  const handleClose = () => {
+    onClose();
+  };
+
+  return (
+    <Modal
+      size="6xl"
+      isOpen={isOpen}
+      onClose={handleClose}
+      portalProps={{ containerRef }}
+      blockScrollOnMount={false}
+      initialFocusRef={submitButtonFocusRef}
+      {...otherProps}
+    >
+      <ModalOverlay />
+      <ModalContent>
+        <ModalHeader>{header}</ModalHeader>
+        <ModalCloseButton />
+        <ModalBody>
+          <Box mb={3}>{children}</Box>
+          <Checkbox
+            mt={4}
+            isChecked={doNotShowAgain}
+            onChange={() =>
+              onDoNotShowAgainChange && onDoNotShowAgainChange(!doNotShowAgain)
+            }
+          >
+            Do not show this again.
+          </Checkbox>
+        </ModalBody>
+        <ModalFooter justifyContent="space-between">
+          <Button colorScheme="gray" onClick={handleClose}>
+            Cancel
+          </Button>
+          {cloneElement(submitButton, { ref: submitButtonFocusRef })}
+        </ModalFooter>
+      </ModalContent>
+    </Modal>
+  );
+};
+
+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 36a145ba26..43c1107d8a 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 (
-    <Menu>
-      <MenuButton
-        as={Button}
-        colorScheme="blue"
-        transition="all 0.2s"
-        title={markLabel}
-        aria-label={markLabel}
-        disabled={!canEdit || isMarkFailedLoading || isMarkSuccessLoading}
-        {...otherProps}
-        mt={2}
+    <>
+      <Menu>
+        <MenuButton
+          as={Button}
+          colorScheme="blue"
+          transition="all 0.2s"
+          title={markLabel}
+          aria-label={markLabel}
+          disabled={!canEdit || isMarkFailedLoading || isMarkSuccessLoading}
+          {...otherProps}
+          mt={2}
+        >
+          <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>
+      <ConfirmationModal
+        isOpen={stateReducer.showConfirmationModal}
+        onClose={() => dispatch({ type: "HIDE_CONFIRMATION_MODAL" })}
+        header="Confirmation"
+        submitButton={
+          <Button
+            onClick={confirmAction}
+            colorScheme={
+              (stateReducer.confirmingAction === "success" && "green") ||
+              (stateReducer.confirmingAction === "failed" && "red") ||
+              "grey"
+            }
+          >
+            Mark as {stateReducer.confirmingAction}
+          </Button>
+        }
+        doNotShowAgain={doNotShowAgain}
+        onDoNotShowAgainChange={(value) => setDoNotShowAgain(value)}
       >
-        <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>
+        Are you sure you want to mark the DAG run as{" "}
+        {stateReducer.confirmingAction}?
+      </ConfirmationModal>
+    </>
   );
 };
 

Reply via email to