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>
+ </>
);
};