Skip to content

Commit

Permalink
confirmation dialog box for DAG run actions (#35393)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
theaadya authored Feb 12, 2024
1 parent 2899cbf commit 002d15a
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 51 deletions.
81 changes: 59 additions & 22 deletions airflow/www/static/js/dag/details/dagRun/ClearRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import React from "react";
import React, { useState } from "react";
import {
Flex,
Button,
Expand All @@ -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");
Expand Down Expand Up @@ -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>
</>
);
};

Expand Down
99 changes: 99 additions & 0 deletions airflow/www/static/js/dag/details/dagRun/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -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;
140 changes: 111 additions & 29 deletions airflow/www/static/js/dag/details/dagRun/MarkRunAs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import React from "react";
import React, { useState, useReducer } from "react";
import {
Flex,
Button,
Expand All @@ -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");
Expand All @@ -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 });
};
Expand All @@ -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>
</>
);
};

Expand Down

0 comments on commit 002d15a

Please sign in to comment.