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

pierrejeambrun 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 e79b034173b Add shortcut to clear and mark state for taskinstance and 
dagrun. (#50885)
e79b034173b is described below

commit e79b034173b4fed704ce61ea30ab8c52c3579bbb
Author: Karthikeyan Singaravelan <[email protected]>
AuthorDate: Mon May 26 14:47:36 2025 +0530

    Add shortcut to clear and mark state for taskinstance and dagrun. (#50885)
    
    * Add shortcut to clear and mark state for taskinstance and dagrun.
    
    * Rename hasHotKey to isHotkeyEnabled.
---
 .../ui/src/components/Clear/Run/ClearRunButton.tsx | 37 +++++++----
 .../Clear/TaskInstance/ClearTaskInstanceButton.tsx | 41 ++++++++----
 .../src/components/MarkAs/Run/MarkRunAsButton.tsx  | 75 ++++++++++++++++------
 .../TaskInstance/MarkTaskInstanceAsButton.tsx      | 75 ++++++++++++++++------
 .../src/airflow/ui/src/pages/Run/Header.tsx        |  4 +-
 .../airflow/ui/src/pages/TaskInstance/Header.tsx   | 12 +++-
 6 files changed, 174 insertions(+), 70 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx 
b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx
index 0395812cd9d..ba9019ba12b 100644
--- a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx
@@ -17,33 +17,46 @@
  * under the License.
  */
 import { Box, useDisclosure } from "@chakra-ui/react";
+import { useHotkeys } from "react-hotkeys-hook";
 import { CgRedo } from "react-icons/cg";
 
 import type { DAGRunResponse } from "openapi/requests/types.gen";
+import { Tooltip } from "src/components/ui";
 import ActionButton from "src/components/ui/ActionButton";
 
 import ClearRunDialog from "./ClearRunDialog";
 
 type Props = {
   readonly dagRun: DAGRunResponse;
+  readonly isHotkeyEnabled?: boolean;
   readonly withText?: boolean;
 };
 
-const ClearRunButton = ({ dagRun, withText = true }: Props) => {
+const ClearRunButton = ({ dagRun, isHotkeyEnabled = false, withText = true }: 
Props) => {
   const { onClose, onOpen, open } = useDisclosure();
 
+  useHotkeys(
+    "shift+c",
+    () => {
+      onOpen();
+    },
+    { enabled: isHotkeyEnabled },
+  );
+
   return (
-    <Box>
-      <ActionButton
-        actionName="Clear Dag Run"
-        icon={<CgRedo />}
-        onClick={onOpen}
-        text="Clear Run"
-        withText={withText}
-      />
-
-      {open ? <ClearRunDialog dagRun={dagRun} onClose={onClose} open={open} /> 
: undefined}
-    </Box>
+    <Tooltip closeDelay={100} content="Press shift+c to clear" 
disabled={!isHotkeyEnabled} openDelay={100}>
+      <Box>
+        <ActionButton
+          actionName="Clear Dag Run"
+          icon={<CgRedo />}
+          onClick={onOpen}
+          text="Clear Run"
+          withText={withText}
+        />
+
+        {open ? <ClearRunDialog dagRun={dagRun} onClose={onClose} open={open} 
/> : undefined}
+      </Box>
+    </Tooltip>
   );
 };
 
diff --git 
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx
 
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx
index 68750c2c949..0badf52acba 100644
--- 
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx
@@ -17,35 +17,48 @@
  * under the License.
  */
 import { Box, useDisclosure } from "@chakra-ui/react";
+import { useHotkeys } from "react-hotkeys-hook";
 import { CgRedo } from "react-icons/cg";
 
 import type { TaskInstanceResponse } from "openapi/requests/types.gen";
+import { Tooltip } from "src/components/ui";
 import ActionButton from "src/components/ui/ActionButton";
 
 import ClearTaskInstanceDialog from "./ClearTaskInstanceDialog";
 
 type Props = {
+  readonly isHotkeyEnabled?: boolean;
   readonly taskInstance: TaskInstanceResponse;
   readonly withText?: boolean;
 };
 
-const ClearTaskInstanceButton = ({ taskInstance, withText = true }: Props) => {
+const ClearTaskInstanceButton = ({ isHotkeyEnabled = false, taskInstance, 
withText = true }: Props) => {
   const { onClose, onOpen, open } = useDisclosure();
 
+  useHotkeys(
+    "shift+c",
+    () => {
+      onOpen();
+    },
+    { enabled: isHotkeyEnabled },
+  );
+
   return (
-    <Box>
-      <ActionButton
-        actionName="Clear Task Instance"
-        icon={<CgRedo />}
-        onClick={onOpen}
-        text="Clear Task Instance"
-        withText={withText}
-      />
-
-      {open ? (
-        <ClearTaskInstanceDialog onClose={onClose} open={open} 
taskInstance={taskInstance} />
-      ) : undefined}
-    </Box>
+    <Tooltip closeDelay={100} content="Press shift+c to clear" 
disabled={!isHotkeyEnabled} openDelay={100}>
+      <Box>
+        <ActionButton
+          actionName="Clear Task Instance"
+          icon={<CgRedo />}
+          onClick={onOpen}
+          text="Clear Task Instance"
+          withText={withText}
+        />
+
+        {open ? (
+          <ClearTaskInstanceDialog onClose={onClose} open={open} 
taskInstance={taskInstance} />
+        ) : undefined}
+      </Box>
+    </Tooltip>
   );
 };
 
diff --git 
a/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx 
b/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx
index 1ed24c51185..ad6fa2ef537 100644
--- a/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx
@@ -18,11 +18,12 @@
  */
 import { Box, useDisclosure } from "@chakra-ui/react";
 import { useState } from "react";
+import { useHotkeys } from "react-hotkeys-hook";
 import { MdArrowDropDown } from "react-icons/md";
 
 import type { DAGRunPatchStates, DAGRunResponse } from 
"openapi/requests/types.gen";
 import { StateBadge } from "src/components/StateBadge";
-import { Menu } from "src/components/ui";
+import { Menu, Tooltip } from "src/components/ui";
 import ActionButton from "src/components/ui/ActionButton";
 
 import { allowedStates } from "../utils";
@@ -30,13 +31,32 @@ import MarkRunAsDialog from "./MarkRunAsDialog";
 
 type Props = {
   readonly dagRun: DAGRunResponse;
+  readonly isHotkeyEnabled?: boolean;
   readonly withText?: boolean;
 };
 
-const MarkRunAsButton = ({ dagRun, withText = true }: Props) => {
+const MarkRunAsButton = ({ dagRun, isHotkeyEnabled = false, withText = true }: 
Props) => {
   const { onClose, onOpen, open } = useDisclosure();
   const [state, setState] = useState<DAGRunPatchStates>("success");
 
+  useHotkeys(
+    "shift+f",
+    () => {
+      setState("failed");
+      onOpen();
+    },
+    { enabled: isHotkeyEnabled && dagRun.state !== "failed" },
+  );
+
+  useHotkeys(
+    "shift+s",
+    () => {
+      setState("success");
+      onOpen();
+    },
+    { enabled: isHotkeyEnabled && dagRun.state !== "success" },
+  );
+
   return (
     <Box>
       <Menu.Root positioning={{ gutter: 0, placement: "bottom" }}>
@@ -50,24 +70,39 @@ const MarkRunAsButton = ({ dagRun, withText = true }: 
Props) => {
           />
         </Menu.Trigger>
         <Menu.Content>
-          {allowedStates.map((menuState) => (
-            <Menu.Item
-              asChild
-              disabled={dagRun.state === menuState}
-              key={menuState}
-              onClick={() => {
-                if (dagRun.state !== menuState) {
-                  setState(menuState);
-                  onOpen();
-                }
-              }}
-              value={menuState}
-            >
-              <StateBadge my={1} state={menuState}>
-                {menuState}
-              </StateBadge>
-            </Menu.Item>
-          ))}
+          {allowedStates.map((menuState) => {
+            const content =
+              menuState === "success"
+                ? "Press shift+s to mark as success"
+                : "Press shift+f to mark as failed";
+
+            return (
+              <Tooltip
+                closeDelay={100}
+                content={content}
+                disabled={!isHotkeyEnabled || dagRun.state === menuState}
+                key={menuState}
+                openDelay={100}
+              >
+                <Menu.Item
+                  asChild
+                  disabled={dagRun.state === menuState}
+                  key={menuState}
+                  onClick={() => {
+                    if (dagRun.state !== menuState) {
+                      setState(menuState);
+                      onOpen();
+                    }
+                  }}
+                  value={menuState}
+                >
+                  <StateBadge my={1} state={menuState}>
+                    {menuState}
+                  </StateBadge>
+                </Menu.Item>
+              </Tooltip>
+            );
+          })}
         </Menu.Content>
       </Menu.Root>
 
diff --git 
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx
 
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx
index 395dfba27b3..e8ffcddd49a 100644
--- 
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx
@@ -18,26 +18,46 @@
  */
 import { Box, useDisclosure } from "@chakra-ui/react";
 import { useState } from "react";
+import { useHotkeys } from "react-hotkeys-hook";
 import { MdArrowDropDown } from "react-icons/md";
 
 import type { TaskInstanceResponse, TaskInstanceState } from 
"openapi/requests/types.gen";
 import { StateBadge } from "src/components/StateBadge";
-import { Menu } from "src/components/ui";
+import { Menu, Tooltip } from "src/components/ui";
 import ActionButton from "src/components/ui/ActionButton";
 
 import { allowedStates } from "../utils";
 import MarkTaskInstanceAsDialog from "./MarkTaskInstanceAsDialog";
 
 type Props = {
+  readonly isHotkeyEnabled?: boolean;
   readonly taskInstance: TaskInstanceResponse;
   readonly withText?: boolean;
 };
 
-const MarkTaskInstanceAsButton = ({ taskInstance, withText = true }: Props) => 
{
+const MarkTaskInstanceAsButton = ({ isHotkeyEnabled = false, taskInstance, 
withText = true }: Props) => {
   const { onClose, onOpen, open } = useDisclosure();
 
   const [state, setState] = useState<TaskInstanceState>("success");
 
+  useHotkeys(
+    "shift+f",
+    () => {
+      setState("failed");
+      onOpen();
+    },
+    { enabled: isHotkeyEnabled && taskInstance.state !== "failed" },
+  );
+
+  useHotkeys(
+    "shift+s",
+    () => {
+      setState("success");
+      onOpen();
+    },
+    { enabled: isHotkeyEnabled && taskInstance.state !== "success" },
+  );
+
   return (
     <Box>
       <Menu.Root positioning={{ gutter: 0, placement: "bottom" }}>
@@ -51,24 +71,39 @@ const MarkTaskInstanceAsButton = ({ taskInstance, withText 
= true }: Props) => {
           />
         </Menu.Trigger>
         <Menu.Content>
-          {allowedStates.map((menuState) => (
-            <Menu.Item
-              asChild
-              disabled={taskInstance.state === menuState}
-              key={menuState}
-              onClick={() => {
-                if (taskInstance.state !== menuState) {
-                  setState(menuState);
-                  onOpen();
-                }
-              }}
-              value={menuState}
-            >
-              <StateBadge my={1} state={menuState}>
-                {menuState}
-              </StateBadge>
-            </Menu.Item>
-          ))}
+          {allowedStates.map((menuState) => {
+            const content =
+              menuState === "success"
+                ? "Press shift+s to mark as success"
+                : "Press shift+f to mark as failed";
+
+            return (
+              <Tooltip
+                closeDelay={100}
+                content={content}
+                disabled={!isHotkeyEnabled || taskInstance.state === menuState}
+                key={menuState}
+                openDelay={100}
+              >
+                <Menu.Item
+                  asChild
+                  disabled={taskInstance.state === menuState}
+                  key={menuState}
+                  onClick={() => {
+                    if (taskInstance.state !== menuState) {
+                      setState(menuState);
+                      onOpen();
+                    }
+                  }}
+                  value={menuState}
+                >
+                  <StateBadge my={1} state={menuState}>
+                    {menuState}
+                  </StateBadge>
+                </Menu.Item>
+              </Tooltip>
+            );
+          })}
         </Menu.Content>
       </Menu.Root>
 
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
index d5bd8d7a655..c412da36b9d 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
@@ -77,8 +77,8 @@ export const Header = ({
               text={Boolean(dagRun.note) ? "Note" : "Add a note"}
               withText={containerWidth > 700}
             />
-            <ClearRunButton dagRun={dagRun} withText={containerWidth > 700} />
-            <MarkRunAsButton dagRun={dagRun} withText={containerWidth > 700} />
+            <ClearRunButton dagRun={dagRun} isHotkeyEnabled 
withText={containerWidth > 700} />
+            <MarkRunAsButton dagRun={dagRun} isHotkeyEnabled 
withText={containerWidth > 700} />
           </>
         }
         icon={<FiBarChart />}
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
index 40c35319b8f..702060ae285 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
@@ -98,8 +98,16 @@ export const Header = ({
               text={Boolean(taskInstance.note) ? "Note" : "Add a note"}
               withText={containerWidth > 700}
             />
-            <ClearTaskInstanceButton taskInstance={taskInstance} 
withText={containerWidth > 700} />
-            <MarkTaskInstanceAsButton taskInstance={taskInstance} 
withText={containerWidth > 700} />
+            <ClearTaskInstanceButton
+              isHotkeyEnabled
+              taskInstance={taskInstance}
+              withText={containerWidth > 700}
+            />
+            <MarkTaskInstanceAsButton
+              isHotkeyEnabled
+              taskInstance={taskInstance}
+              withText={containerWidth > 700}
+            />
           </>
         }
         icon={<MdOutlineTask />}

Reply via email to