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 81ef598fd8d Improve Grid view UX (#54846)
81ef598fd8d is described below

commit 81ef598fd8dab4400ef06f1835e7212c574e2613
Author: Brent Bovenzi <[email protected]>
AuthorDate: Wed Aug 27 17:16:41 2025 -0500

    Improve Grid view UX (#54846)
    
    * Improve grid kbd navigation
    
    * Use chakra tooltip
    
    * Fix static checks
    
    * Fix static checks
    
    * Upgrade keyboard commands to not conflict with regular arrow actions
    
    * Remove jump hotkeys
    
    * Remove english translation for jump
---
 .../src/airflow/ui/public/i18n/locales/ar/dag.json |   1 -
 .../src/airflow/ui/public/i18n/locales/de/dag.json |   3 +-
 .../src/airflow/ui/public/i18n/locales/en/dag.json |   3 +-
 .../src/airflow/ui/public/i18n/locales/he/dag.json |   1 -
 .../src/airflow/ui/public/i18n/locales/ko/dag.json |   1 -
 .../src/airflow/ui/public/i18n/locales/nl/dag.json |   1 -
 .../src/airflow/ui/public/i18n/locales/pl/dag.json |   1 -
 .../src/airflow/ui/public/i18n/locales/tr/dag.json |   1 -
 .../airflow/ui/public/i18n/locales/zh-TW/dag.json  |   1 -
 .../src/airflow/ui/src/hooks/navigation/types.ts   |   5 +-
 .../src/hooks/navigation/useKeyboardNavigation.ts  |  22 +-
 .../ui/src/hooks/navigation/useNavigation.ts       |  40 +--
 .../ui/src/layouts/Details/DetailsLayout.tsx       |  35 ++-
 .../airflow/ui/src/layouts/Details/Grid/Grid.tsx   |  49 +---
 .../airflow/ui/src/layouts/Details/Grid/GridTI.tsx | 134 +++------
 .../ui/src/layouts/Details/Grid/useGridStore.ts    |  28 --
 .../ui/src/layouts/Details/PanelButtons.tsx        | 323 +++++++++++----------
 .../ui/src/layouts/Details/ToggleGroups.tsx        |   4 +-
 18 files changed, 259 insertions(+), 394 deletions(-)

diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json
index 800daf360b7..3a9483a3faa 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json
@@ -40,7 +40,6 @@
     "warning": "تحذير"
   },
   "navigation": {
-    "jump": "الانتقال: Shift+{{arrow}}",
     "navigation": "التنقل: {{arrow}}",
     "toggleGroup": "تبديل المجموعة: المسافة"
   },
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
index adf8c6da98b..1c3a646b700 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
@@ -40,8 +40,7 @@
     "warning": "WARNING"
   },
   "navigation": {
-    "jump": "Springen: Umschalttaste+{{arrow}}",
-    "navigation": "Navigation: {{arrow}}",
+    "navigation": "Navigation: Umschalttaste+{{arrow}}",
     "toggleGroup": "Gruppen umschalten: Leertaste"
   },
   "overview": {
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
index 5a66838365d..215c4161d8c 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
@@ -67,8 +67,7 @@
     "warning": "WARNING"
   },
   "navigation": {
-    "jump": "Jump: Shift+{{arrow}}",
-    "navigation": "Navigation: {{arrow}}",
+    "navigation": "Navigation: Shift+{{arrow}}",
     "toggleGroup": "Toggle group: Space"
   },
   "overview": {
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json
index d7b58d0d4d2..8e3fd86e062 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json
@@ -40,7 +40,6 @@
     "warning": "WARNING"
   },
   "navigation": {
-    "jump": "קפיצה: Shift+{{arrow}}",
     "navigation": "ניווט: {{arrow}}",
     "toggleGroup": "החלפת קבוצה: רווח"
   },
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
index 722b8d36734..ef7106c7d95 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
@@ -40,7 +40,6 @@
     "warning": "경고"
   },
   "navigation": {
-    "jump": "이동: Shift+{{arrow}}",
     "navigation": "탐색: {{arrow}}",
     "toggleGroup": "그룹 전환: Space"
   },
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json
index 88f1305adea..9fc4737f8cb 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json
@@ -40,7 +40,6 @@
     "warning": "WARNING"
   },
   "navigation": {
-    "jump": "Verspringen: Shift+{{arrow}}",
     "navigation": "Navigatie: {{arrow}}",
     "toggleGroup": "Groep in-/uitklappen: Spatie"
   },
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
index a8e48aa91fd..455a08cc9a3 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
@@ -40,7 +40,6 @@
     "warning": "WARNING"
   },
   "navigation": {
-    "jump": "Przeskocz: Shift+{{arrow}}",
     "navigation": "Przewiń: {{arrow}}",
     "toggleGroup": "Przełącz grupę: Space"
   },
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json
index ab14e119fcd..242e09d72ae 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json
@@ -40,7 +40,6 @@
     "warning": "UYARI"
   },
   "navigation": {
-    "jump": "Atla: Shift+{{arrow}}",
     "navigation": "Gezin: {{arrow}}",
     "toggleGroup": "Grubu aç/kapat: Space"
   },
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
index 946efc199f7..0b7b06e7cfb 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
@@ -68,7 +68,6 @@
     "warning": "WARNING"
   },
   "navigation": {
-    "jump": "跳躍: Shift+{{arrow}}",
     "navigation": "導航: {{arrow}}",
     "toggleGroup": "展開/收合群組: 空白鍵"
   },
diff --git a/airflow-core/src/airflow/ui/src/hooks/navigation/types.ts 
b/airflow-core/src/airflow/ui/src/hooks/navigation/types.ts
index 890663da4a4..a3a7a1c9a31 100644
--- a/airflow-core/src/airflow/ui/src/hooks/navigation/types.ts
+++ b/airflow-core/src/airflow/ui/src/hooks/navigation/types.ts
@@ -31,8 +31,6 @@ export type NavigationIndices = {
 };
 
 export type UseNavigationProps = {
-  enabled?: boolean;
-  onEscapePress?: () => void;
   onToggleGroup?: (taskId: string) => void;
   runs: Array<GridRunsResponse>;
   tasks: Array<GridTask>;
@@ -41,8 +39,7 @@ export type UseNavigationProps = {
 export type UseNavigationReturn = {
   currentIndices: NavigationIndices;
   currentTask: GridTask | undefined;
-  enabled: boolean;
-  handleNavigation: (direction: NavigationDirection, isJump?: boolean) => void;
+  handleNavigation: (direction: NavigationDirection) => void;
   mode: NavigationMode;
   setMode: (mode: NavigationMode) => void;
 };
diff --git 
a/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts 
b/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts
index 99b8df2522d..1d8277d2c0c 100644
--- a/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts
+++ b/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts
@@ -21,13 +21,11 @@ import { useHotkeys } from "react-hotkeys-hook";
 
 import type { ArrowKey, NavigationDirection } from "./types";
 
-const ARROW_KEYS = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"] as 
const;
-const JUMP_KEYS = ["shift+ArrowDown", "shift+ArrowUp", "shift+ArrowLeft", 
"shift+ArrowRight"] as const;
+const ARROW_KEYS = ["shift+ArrowDown", "shift+ArrowUp", "shift+ArrowLeft", 
"shift+ArrowRight"] as const;
 
 type Props = {
   enabled?: boolean;
-  onEscapePress?: () => void;
-  onNavigate: (direction: NavigationDirection, isJump?: boolean) => void;
+  onNavigate: (direction: NavigationDirection) => void;
   onToggleGroup?: () => void;
 };
 
@@ -46,32 +44,24 @@ const mapKeyToDirection = (key: ArrowKey): 
NavigationDirection => {
   }
 };
 
-export const useKeyboardNavigation = ({
-  enabled = true,
-  onEscapePress,
-  onNavigate,
-  onToggleGroup,
-}: Props) => {
+export const useKeyboardNavigation = ({ enabled = true, onNavigate, 
onToggleGroup }: Props) => {
   const createKeyHandler = useCallback(
-    (isJump: boolean) => (event: KeyboardEvent) => {
+    () => (event: KeyboardEvent) => {
       const direction = mapKeyToDirection(event.key as ArrowKey);
 
       event.preventDefault();
       event.stopPropagation();
 
-      onNavigate(direction, isJump);
+      onNavigate(direction);
     },
     [onNavigate],
   );
 
-  const handleNormalKeyPress = createKeyHandler(false);
-  const handleJumpKeyPress = createKeyHandler(true);
+  const handleNormalKeyPress = createKeyHandler();
 
   const hotkeyOptions = { enabled, preventDefault: true };
 
   useHotkeys(ARROW_KEYS.join(","), handleNormalKeyPress, hotkeyOptions, 
[onNavigate]);
-  useHotkeys(JUMP_KEYS.join(","), handleJumpKeyPress, hotkeyOptions, 
[onNavigate]);
 
   useHotkeys("space", () => onToggleGroup?.(), hotkeyOptions, [onToggleGroup]);
-  useHotkeys("escape", () => onEscapePress?.(), hotkeyOptions, 
[onEscapePress]);
 };
diff --git a/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts 
b/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts
index 13eec1751c7..9c0bb7bdd64 100644
--- a/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts
+++ b/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts
@@ -59,17 +59,8 @@ const isValidDirection = (direction: NavigationDirection, 
mode: NavigationMode):
   }
 };
 
-const getNextIndex = (
-  current: number,
-  direction: number,
-  options: { isJump: boolean; max: number },
-): number => {
-  if (options.isJump) {
-    return direction > 0 ? options.max - 1 : 0;
-  }
-
-  return Math.max(0, Math.min(options.max - 1, current + direction));
-};
+const getNextIndex = (current: number, direction: number, options: { max: 
number }): number =>
+  Math.max(0, Math.min(options.max - 1, current + direction));
 
 const buildPath = (params: {
   dagId: string;
@@ -106,14 +97,9 @@ const buildPath = (params: {
   }
 };
 
-export const useNavigation = ({
-  enabled = true,
-  onEscapePress,
-  onToggleGroup,
-  runs,
-  tasks,
-}: UseNavigationProps): UseNavigationReturn => {
+export const useNavigation = ({ onToggleGroup, runs, tasks }: 
UseNavigationProps): UseNavigationReturn => {
   const { dagId = "", groupId = "", mapIndex = "-1", runId = "", taskId = "" } 
= useParams();
+  const enabled = Boolean(dagId) && (Boolean(runId) || Boolean(taskId) || 
Boolean(groupId));
   const navigate = useNavigate();
   const [mode, setMode] = useState<NavigationMode>("TI");
 
@@ -137,7 +123,7 @@ export const useNavigation = ({
   const currentTask = useMemo(() => tasks[currentIndices.taskIndex], [tasks, 
currentIndices.taskIndex]);
 
   const handleNavigation = useCallback(
-    (direction: NavigationDirection, isJump: boolean = false) => {
+    (direction: NavigationDirection) => {
       if (!enabled || !dagId || !isValidDirection(direction, mode)) {
         return;
       }
@@ -151,7 +137,7 @@ export const useNavigation = ({
 
       const isAtBoundary = boundaries[direction];
 
-      if (!isJump && isAtBoundary) {
+      if (isAtBoundary) {
         return;
       }
 
@@ -171,11 +157,10 @@ export const useNavigation = ({
 
       if (nav.index === "taskIndex") {
         newIndices.taskIndex = getNextIndex(currentIndices.taskIndex, 
nav.direction, {
-          isJump,
           max: nav.max,
         });
       } else {
-        newIndices.runIndex = getNextIndex(currentIndices.runIndex, 
nav.direction, { isJump, max: nav.max });
+        newIndices.runIndex = getNextIndex(currentIndices.runIndex, 
nav.direction, { max: nav.max });
       }
 
       const { runIndex: newRunIndex, taskIndex: newTaskIndex } = newIndices;
@@ -191,14 +176,20 @@ export const useNavigation = ({
         const path = buildPath({ dagId, mapIndex, mode, run, task });
 
         navigate(path, { replace: true });
+
+        const grid = 
document.querySelector(`[id='grid-${run.run_id}-${task.id}']`);
+
+        // Set the focus to the grid link to allow a user to continue tabbing 
through with the keyboard
+        if (grid) {
+          (grid as HTMLLinkElement).focus();
+        }
       }
     },
     [currentIndices, dagId, enabled, mapIndex, mode, runs, tasks, navigate],
   );
 
   useKeyboardNavigation({
-    enabled: enabled && Boolean(dagId),
-    onEscapePress,
+    enabled,
     onNavigate: handleNavigation,
     onToggleGroup: currentTask?.isGroup && onToggleGroup ? () => 
onToggleGroup(currentTask.id) : undefined,
   });
@@ -206,7 +197,6 @@ export const useNavigation = ({
   return {
     currentIndices,
     currentTask,
-    enabled: enabled && Boolean(dagId),
     handleNavigation,
     mode,
     setMode,
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
index e536061cded..4a3714ac83b 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
@@ -90,15 +90,14 @@ export const DetailsLayout = ({ children, error, isLoading, 
tabs }: Props) => {
           <Tooltip content={translate("common:showDetailsPanel")}>
             <IconButton
               aria-label={translate("common:showDetailsPanel")}
-              bg="bg.surface"
-              borderRadius="full"
+              bg="fg.subtle"
+              borderRadius={direction === "ltr" ? "100% 0 0 100%" : "0 100% 
100% 0"}
               boxShadow="md"
-              cursor="pointer"
-              left={direction === "rtl" ? 0 : undefined}
+              left={direction === "rtl" ? "-5px" : undefined}
               onClick={() => setIsRightPanelCollapsed(false)}
               position="absolute"
-              right={direction === "ltr" ? 0 : undefined}
-              size="sm"
+              right={direction === "ltr" ? "-5px" : undefined}
+              size="2xs"
               top="50%"
               zIndex={10}
             >
@@ -160,25 +159,31 @@ export const DetailsLayout = ({ children, error, 
isLoading, tabs }: Props) => {
                   justifyContent="center"
                   position="relative"
                   w={0.5}
-                >
+                  // onClick={(e) => console.log(e)}
+                />
+              </PanelResizeHandle>
+
+              {/* Collapse button positioned next to the resize handle */}
+
+              <Panel defaultSize={dagView === "graph" ? 30 : 80} 
id="details-panel" minSize={20} order={2}>
+                <Box display="flex" flexDirection="column" h="100%" 
position="relative">
                   <Tooltip content={translate("common:collapseDetailsPanel")}>
                     <IconButton
                       aria-label={translate("common:collapseDetailsPanel")}
-                      bg="bg.surface"
-                      borderRadius="full"
+                      bg="fg.subtle"
+                      borderRadius={direction === "ltr" ? "0 100% 100% 0" : 
"100% 0 0 100%"}
                       boxShadow="md"
-                      cursor="pointer"
+                      left={direction === "ltr" ? "-5px" : undefined}
                       onClick={() => setIsRightPanelCollapsed(true)}
-                      size="xs"
+                      position="absolute"
+                      right={direction === "rtl" ? "-5px" : undefined}
+                      size="2xs"
+                      top="50%"
                       zIndex={2}
                     >
                       {direction === "ltr" ? <FaChevronRight /> : 
<FaChevronLeft />}
                     </IconButton>
                   </Tooltip>
-                </Box>
-              </PanelResizeHandle>
-              <Panel defaultSize={dagView === "graph" ? 30 : 80} 
id="details-panel" minSize={20} order={2}>
-                <Box display="flex" flexDirection="column" h="100%">
                   {children}
                   {Boolean(error) || (warningData?.dag_warnings.length ?? 0) > 
0 ? (
                     <>
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
index a5b1fb44298..8a56fc47441 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
@@ -16,10 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Flex, IconButton, Text } from "@chakra-ui/react";
+import { Box, Flex, IconButton } from "@chakra-ui/react";
 import dayjs from "dayjs";
 import dayjsDuration from "dayjs/plugin/duration";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
 import { useTranslation } from "react-i18next";
 import { FiChevronsRight } from "react-icons/fi";
 import { Link, useParams } from "react-router-dom";
@@ -35,24 +35,10 @@ import { Bar } from "./Bar";
 import { DurationAxis } from "./DurationAxis";
 import { DurationTick } from "./DurationTick";
 import { TaskNames } from "./TaskNames";
-import { useGridStore } from "./useGridStore";
 import { flattenNodes } from "./utils";
 
 dayjs.extend(dayjsDuration);
 
-const getArrowsForMode = (navigationMode: string) => {
-  switch (navigationMode) {
-    case "run":
-      return "←→";
-    case "task":
-      return "↑↓";
-    case "TI":
-      return "↑↓←→";
-    default:
-      return "↑↓←→";
-  }
-};
-
 type Props = {
   readonly limit: number;
   readonly showGantt?: boolean;
@@ -61,7 +47,6 @@ type Props = {
 export const Grid = ({ limit, showGantt }: Props) => {
   const { t: translate } = useTranslation("dag");
   const gridRef = useRef<HTMLDivElement>(null);
-  const { isGridFocused, setIsGridFocused } = useGridStore();
 
   const [selectedIsVisible, setSelectedIsVisible] = useState<boolean | 
undefined>();
   const [hasActiveRun, setHasActiveRun] = useState<boolean | undefined>();
@@ -106,21 +91,7 @@ export const Grid = ({ limit, showGantt }: Props) => {
 
   const { flatNodes } = useMemo(() => flattenNodes(dagStructure, 
openGroupIds), [dagStructure, openGroupIds]);
 
-  const setGridFocus = useCallback(
-    (focused: boolean) => {
-      setIsGridFocused(focused);
-      if (focused) {
-        gridRef.current?.focus();
-      } else {
-        gridRef.current?.blur();
-      }
-    },
-    [setIsGridFocused],
-  );
-
-  const { mode, setMode } = useNavigation({
-    enabled: isGridFocused,
-    onEscapePress: () => setGridFocus(false),
+  const { setMode } = useNavigation({
     onToggleGroup: toggleGroupId,
     runs: gridRuns ?? [],
     tasks: flatNodes,
@@ -128,15 +99,7 @@ export const Grid = ({ limit, showGantt }: Props) => {
 
   return (
     <Flex
-      _focus={{
-        borderRadius: "4px",
-        boxShadow: "0 0 0 2px rgba(59, 130, 246, 0.5)",
-      }}
-      cursor="pointer"
       justifyContent="flex-start"
-      onBlur={() => setGridFocus(false)}
-      onFocus={() => setGridFocus(true)}
-      onMouseDown={() => setGridFocus(true)}
       outline="none"
       position="relative"
       pt={20}
@@ -144,12 +107,6 @@ export const Grid = ({ limit, showGantt }: Props) => {
       tabIndex={0}
       width={showGantt ? undefined : "100%"}
     >
-      <Box borderRadius="md" color="gray.400" fontSize="xs" 
position="absolute" px={0} py={12} top={0}>
-        <Text>{translate("navigation.navigation", { arrow: 
getArrowsForMode(mode) })}</Text>
-        <Text>{translate("navigation.jump", { arrow: getArrowsForMode(mode) 
})}</Text>
-        {mode !== "run" && <Text>{translate("navigation.toggleGroup")}</Text>}
-      </Box>
-
       <Box flexGrow={1} minWidth={7} position="relative" top="100px">
         <TaskNames nodes={flatNodes} onRowClick={() => setMode("task")} />
       </Box>
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
index 89ea4f30688..e03ff24d47b 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
@@ -16,15 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Badge, chakra, Flex } from "@chakra-ui/react";
+import { Badge, Flex } from "@chakra-ui/react";
 import type { MouseEvent } from "react";
-import React, { useRef } from "react";
+import React from "react";
 import { useTranslation } from "react-i18next";
 import { Link, useParams } from "react-router-dom";
 
 import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen";
 import { StateIcon } from "src/components/StateIcon";
 import Time from "src/components/Time";
+import { Tooltip } from "src/components/ui";
 
 type Props = {
   readonly dagId: string;
@@ -57,41 +58,6 @@ const onMouseLeave = (event: MouseEvent<HTMLDivElement>) => {
 const Instance = ({ dagId, instance, isGroup, isMapped, onClick, runId, 
search, taskId }: Props) => {
   const { groupId: selectedGroupId, taskId: selectedTaskId } = useParams();
   const { t: translate } = useTranslation();
-  const debounceTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
-  const tooltipRef = useRef<HTMLElement | undefined>(undefined);
-
-  const onBadgeMouseEnter = (event: MouseEvent<HTMLDivElement>) => {
-    // Clear any existing timeout
-    if (debounceTimeoutRef.current) {
-      clearTimeout(debounceTimeoutRef.current);
-    }
-
-    // Store reference to the tooltip element
-    const tooltip = event.currentTarget.querySelector("#tooltip") as 
HTMLElement;
-
-    tooltipRef.current = tooltip;
-
-    // Set a new timeout to show the tooltip after 200ms
-    debounceTimeoutRef.current = setTimeout(() => {
-      if (tooltipRef.current) {
-        tooltipRef.current.style.visibility = "visible";
-      }
-    }, 200);
-  };
-
-  const onBadgeMouseLeave = () => {
-    // Clear any existing timeout
-    if (debounceTimeoutRef.current) {
-      clearTimeout(debounceTimeoutRef.current);
-      debounceTimeoutRef.current = undefined;
-    }
-
-    // Hide the tooltip immediately
-    if (tooltipRef.current) {
-      tooltipRef.current.style.visibility = "hidden";
-      tooltipRef.current = undefined;
-    }
-  };
 
   return (
     <Flex
@@ -103,12 +69,13 @@ const Instance = ({ dagId, instance, isGroup, isMapped, 
onClick, runId, search,
       key={taskId}
       onMouseEnter={onMouseEnter}
       onMouseLeave={onMouseLeave}
+      position="relative"
       px="2px"
       py={0}
       transition="background-color 0.2s"
-      zIndex={1}
     >
       <Link
+        id={`grid-${runId}-${taskId}`}
         onClick={onClick}
         replace
         to={{
@@ -116,65 +83,42 @@ const Instance = ({ dagId, instance, isGroup, isMapped, 
onClick, runId, search,
           search,
         }}
       >
-        <Badge
-          borderRadius={4}
-          colorPalette={instance.state ?? "none"}
-          height="14px"
-          minH={0}
-          onMouseEnter={onBadgeMouseEnter}
-          onMouseLeave={onBadgeMouseLeave}
-          p={0}
-          position="relative"
-          variant="solid"
-          width="14px"
+        <Tooltip
+          content={
+            <>
+              {translate("taskId")}: {taskId}
+              <br />
+              {translate("state")}: {instance.state}
+              {instance.min_start_date !== null && (
+                <>
+                  <br />
+                  {translate("startDate")}: <Time 
datetime={instance.min_start_date} />
+                </>
+              )}
+              {instance.max_end_date !== null && (
+                <>
+                  <br />
+                  {translate("endDate")}: <Time 
datetime={instance.max_end_date} />
+                </>
+              )}
+            </>
+          }
         >
-          <StateIcon
-            size={10}
-            state={instance.state}
-            style={{
-              marginLeft: "2px",
-            }}
-          />
-          <chakra.span
-            bg="bg.inverted"
-            borderRadius={2}
-            bottom={0}
-            color="fg.inverted"
-            id="tooltip"
-            p={2}
-            position="absolute"
-            right={5}
-            visibility="hidden"
-            zIndex="tooltip"
+          <Badge
+            alignItems="center"
+            borderRadius={4}
+            colorPalette={instance.state ?? "none"}
+            display="flex"
+            height="14px"
+            justifyContent="center"
+            minH={0}
+            p={0}
+            variant="solid"
+            width="14px"
           >
-            {translate("taskId")}: {taskId}
-            <br />
-            {translate("state")}: {instance.state}
-            {instance.min_start_date !== null && (
-              <>
-                <br />
-                {translate("startDate")}: <Time 
datetime={instance.min_start_date} />
-              </>
-            )}
-            {instance.max_end_date !== null && (
-              <>
-                <br />
-                {translate("endDate")}: <Time datetime={instance.max_end_date} 
/>
-              </>
-            )}
-            {/* Tooltip arrow pointing to the badge */}
-            <chakra.div
-              bg="bg.inverted"
-              borderRadius={1}
-              bottom={1}
-              height={2}
-              position="absolute"
-              right="-3px"
-              transform="rotate(45deg)"
-              width={2}
-            />
-          </chakra.span>
-        </Badge>
+            <StateIcon size={10} state={instance.state} />
+          </Badge>
+        </Tooltip>
       </Link>
     </Flex>
   );
diff --git 
a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridStore.ts 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridStore.ts
deleted file mode 100644
index 7f580649932..00000000000
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridStore.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*!
- * 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 { create } from "zustand";
-
-export const useGridStore = create<{ isGridFocused: boolean; setIsGridFocused: 
(value: boolean) => void }>(
-  (set) => ({
-    // isGridFocused is shared between different pages (Run, GroupInstance, 
MappedInstance, TaskInstance, etc.).
-    // This will avoid many prop drilling and allow proper refocus of the grid 
when navigating between these pages via grid links.
-    isGridFocused: false,
-    setIsGridFocused: (value: boolean) => set({ isGridFocused: value }),
-  }),
-);
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
index 2684be54a64..75262ba3cf8 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
@@ -28,17 +28,20 @@ import {
   Portal,
   Select,
   VStack,
+  Text,
+  Box,
 } from "@chakra-ui/react";
 import { useReactFlow } from "@xyflow/react";
 import { useTranslation } from "react-i18next";
 import { FiChevronDown, FiGrid } from "react-icons/fi";
+import { LuKeyboard } from "react-icons/lu";
 import { MdOutlineAccountTree } from "react-icons/md";
 import { useParams } from "react-router-dom";
 import { useLocalStorage } from "usehooks-ts";
 
 import { DagVersionSelect } from "src/components/DagVersionSelect";
 import { directionOptions, type Direction } from 
"src/components/Graph/useGraphLayout";
-import { Button } from "src/components/ui";
+import { Button, Tooltip } from "src/components/ui";
 import { Checkbox } from "src/components/ui/Checkbox";
 
 import { DagRunSelect } from "./DagRunSelect";
@@ -135,156 +138,174 @@ export const PanelButtons = ({
   };
 
   return (
-    <Flex justifyContent="space-between" position="absolute" top={1} 
width="100%" zIndex={1}>
-      <ButtonGroup attached size="sm" variant="outline">
-        <IconButton
-          aria-label={translate("dag:panel.buttons.showGrid")}
-          colorPalette="blue"
-          onClick={() => {
-            setDagView("grid");
-            if (dagView === "grid") {
-              handleFocus("grid");
-            }
-          }}
-          title={translate("dag:panel.buttons.showGrid")}
-          variant={dagView === "grid" ? "solid" : "outline"}
-        >
-          <FiGrid />
-        </IconButton>
-        <IconButton
-          aria-label={translate("dag:panel.buttons.showGraph")}
-          colorPalette="blue"
-          onClick={() => {
-            setDagView("graph");
-            if (dagView === "graph") {
-              handleFocus("graph");
-            }
-          }}
-          title={translate("dag:panel.buttons.showGraph")}
-          variant={dagView === "graph" ? "solid" : "outline"}
-        >
-          <MdOutlineAccountTree />
-        </IconButton>
-      </ButtonGroup>
-      <Flex gap={1} mr={3}>
-        <ToggleGroups />
-        {/* eslint-disable-next-line jsx-a11y/no-autofocus */}
-        <Popover.Root autoFocus={false} positioning={{ placement: "bottom-end" 
}}>
-          <Popover.Trigger asChild>
-            <Button size="sm" variant="outline">
-              {translate("dag:panel.buttons.options")}
-              <FiChevronDown size="0.5rem" />
-            </Button>
-          </Popover.Trigger>
-          <Portal>
-            <Popover.Positioner>
-              <Popover.Content>
-                <Popover.Arrow />
-                <Popover.Body display="flex" flexDirection="column" gap={4} 
p={2}>
-                  {dagView === "graph" ? (
-                    <>
-                      <DagVersionSelect />
-                      <DagRunSelect limit={limit} />
-                      <Select.Root
-                        // @ts-expect-error The expected option type is 
incorrect
-                        collection={getOptions(translate)}
-                        data-testid="dependencies"
-                        onValueChange={handleDepsChange}
-                        size="sm"
-                        value={[dependencies]}
-                      >
-                        <Select.Label 
fontSize="xs">{translate("dag:panel.dependencies.label")}</Select.Label>
-                        <Select.Control>
-                          <Select.Trigger>
-                            <Select.ValueText 
placeholder={translate("dag:panel.dependencies.label")} />
-                          </Select.Trigger>
-                          <Select.IndicatorGroup>
-                            <Select.Indicator />
-                          </Select.IndicatorGroup>
-                        </Select.Control>
-                        <Select.Positioner>
-                          <Select.Content>
-                            {getOptions(translate).items.map((option) => (
-                              <Select.Item item={option} key={option.value}>
-                                {option.label}
-                              </Select.Item>
-                            ))}
-                          </Select.Content>
-                        </Select.Positioner>
-                      </Select.Root>
-                      <Select.Root
-                        // @ts-expect-error The expected option type is 
incorrect
-                        collection={directionOptions(translate)}
-                        onValueChange={handleDirectionUpdate}
-                        size="sm"
-                        value={[direction]}
-                      >
-                        <Select.Label fontSize="xs">
-                          {translate("dag:panel.graphDirection.label")}
-                        </Select.Label>
-                        <Select.Control>
-                          <Select.Trigger>
-                            <Select.ValueText />
-                          </Select.Trigger>
-                          <Select.IndicatorGroup>
-                            <Select.Indicator />
-                          </Select.IndicatorGroup>
-                        </Select.Control>
-                        <Select.Positioner>
-                          <Select.Content>
-                            {directionOptions(translate).items.map((option) => 
(
-                              <Select.Item item={option} key={option.value}>
-                                {option.label}
-                              </Select.Item>
-                            ))}
-                          </Select.Content>
-                        </Select.Positioner>
-                      </Select.Root>
-                    </>
-                  ) : (
-                    <>
-                      <Select.Root
-                        // @ts-expect-error The expected option type is 
incorrect
-                        collection={displayRunOptions}
-                        data-testid="display-dag-run-options"
-                        onValueChange={handleLimitChange}
-                        size="sm"
-                        value={[limit.toString()]}
-                      >
-                        
<Select.Label>{translate("dag:panel.dagRuns.label")}</Select.Label>
-                        <Select.Control>
-                          <Select.Trigger>
-                            <Select.ValueText />
-                          </Select.Trigger>
-                          <Select.IndicatorGroup>
-                            <Select.Indicator />
-                          </Select.IndicatorGroup>
-                        </Select.Control>
-                        <Select.Positioner>
-                          <Select.Content>
-                            {displayRunOptions.items.map((option) => (
-                              <Select.Item item={option} key={option.value}>
-                                {option.label}
-                              </Select.Item>
-                            ))}
-                          </Select.Content>
-                        </Select.Positioner>
-                      </Select.Root>
-                      {shouldShowToggleButtons ? (
-                        <VStack alignItems="flex-start" px={1}>
-                          <Checkbox checked={showGantt} onChange={() => 
setShowGantt(!showGantt)} size="sm">
-                            {translate("dag:panel.buttons.showGantt")}
-                          </Checkbox>
-                        </VStack>
-                      ) : undefined}
-                    </>
-                  )}
-                </Popover.Body>
-              </Popover.Content>
-            </Popover.Positioner>
-          </Portal>
-        </Popover.Root>
+    <Box position="absolute" top={1} width="100%" zIndex={1}>
+      <Flex flexWrap="wrap" justifyContent="space-between">
+        <ButtonGroup attached size="sm" variant="outline">
+          <IconButton
+            aria-label={translate("dag:panel.buttons.showGrid")}
+            colorPalette="blue"
+            onClick={() => {
+              setDagView("grid");
+              if (dagView === "grid") {
+                handleFocus("grid");
+              }
+            }}
+            title={translate("dag:panel.buttons.showGrid")}
+            variant={dagView === "grid" ? "solid" : "outline"}
+          >
+            <FiGrid />
+          </IconButton>
+          <IconButton
+            aria-label={translate("dag:panel.buttons.showGraph")}
+            colorPalette="blue"
+            onClick={() => {
+              setDagView("graph");
+              if (dagView === "graph") {
+                handleFocus("graph");
+              }
+            }}
+            title={translate("dag:panel.buttons.showGraph")}
+            variant={dagView === "graph" ? "solid" : "outline"}
+          >
+            <MdOutlineAccountTree />
+          </IconButton>
+        </ButtonGroup>
+        <Flex gap={1}>
+          <ToggleGroups />
+          {/* eslint-disable-next-line jsx-a11y/no-autofocus */}
+          <Popover.Root autoFocus={false} positioning={{ placement: 
"bottom-end" }}>
+            <Popover.Trigger asChild>
+              <Button size="sm" variant="outline">
+                {translate("dag:panel.buttons.options")}
+                <FiChevronDown size="0.5rem" />
+              </Button>
+            </Popover.Trigger>
+            <Portal>
+              <Popover.Positioner>
+                <Popover.Content>
+                  <Popover.Arrow />
+                  <Popover.Body display="flex" flexDirection="column" gap={4} 
p={2}>
+                    {dagView === "graph" ? (
+                      <>
+                        <DagVersionSelect />
+                        <DagRunSelect limit={limit} />
+                        <Select.Root
+                          // @ts-expect-error The expected option type is 
incorrect
+                          collection={getOptions(translate)}
+                          data-testid="dependencies"
+                          onValueChange={handleDepsChange}
+                          size="sm"
+                          value={[dependencies]}
+                        >
+                          <Select.Label fontSize="xs">
+                            {translate("dag:panel.dependencies.label")}
+                          </Select.Label>
+                          <Select.Control>
+                            <Select.Trigger>
+                              <Select.ValueText 
placeholder={translate("dag:panel.dependencies.label")} />
+                            </Select.Trigger>
+                            <Select.IndicatorGroup>
+                              <Select.Indicator />
+                            </Select.IndicatorGroup>
+                          </Select.Control>
+                          <Select.Positioner>
+                            <Select.Content>
+                              {getOptions(translate).items.map((option) => (
+                                <Select.Item item={option} key={option.value}>
+                                  {option.label}
+                                </Select.Item>
+                              ))}
+                            </Select.Content>
+                          </Select.Positioner>
+                        </Select.Root>
+                        <Select.Root
+                          // @ts-expect-error The expected option type is 
incorrect
+                          collection={directionOptions(translate)}
+                          onValueChange={handleDirectionUpdate}
+                          size="sm"
+                          value={[direction]}
+                        >
+                          <Select.Label fontSize="xs">
+                            {translate("dag:panel.graphDirection.label")}
+                          </Select.Label>
+                          <Select.Control>
+                            <Select.Trigger>
+                              <Select.ValueText />
+                            </Select.Trigger>
+                            <Select.IndicatorGroup>
+                              <Select.Indicator />
+                            </Select.IndicatorGroup>
+                          </Select.Control>
+                          <Select.Positioner>
+                            <Select.Content>
+                              {directionOptions(translate).items.map((option) 
=> (
+                                <Select.Item item={option} key={option.value}>
+                                  {option.label}
+                                </Select.Item>
+                              ))}
+                            </Select.Content>
+                          </Select.Positioner>
+                        </Select.Root>
+                      </>
+                    ) : (
+                      <>
+                        <Select.Root
+                          // @ts-expect-error The expected option type is 
incorrect
+                          collection={displayRunOptions}
+                          data-testid="display-dag-run-options"
+                          onValueChange={handleLimitChange}
+                          size="sm"
+                          value={[limit.toString()]}
+                        >
+                          
<Select.Label>{translate("dag:panel.dagRuns.label")}</Select.Label>
+                          <Select.Control>
+                            <Select.Trigger>
+                              <Select.ValueText />
+                            </Select.Trigger>
+                            <Select.IndicatorGroup>
+                              <Select.Indicator />
+                            </Select.IndicatorGroup>
+                          </Select.Control>
+                          <Select.Positioner>
+                            <Select.Content>
+                              {displayRunOptions.items.map((option) => (
+                                <Select.Item item={option} key={option.value}>
+                                  {option.label}
+                                </Select.Item>
+                              ))}
+                            </Select.Content>
+                          </Select.Positioner>
+                        </Select.Root>
+                        {shouldShowToggleButtons ? (
+                          <VStack alignItems="flex-start" px={1}>
+                            <Checkbox checked={showGantt} onChange={() => 
setShowGantt(!showGantt)} size="sm">
+                              {translate("dag:panel.buttons.showGantt")}
+                            </Checkbox>
+                          </VStack>
+                        ) : undefined}
+                      </>
+                    )}
+                  </Popover.Body>
+                </Popover.Content>
+              </Popover.Positioner>
+            </Portal>
+          </Popover.Root>
+        </Flex>
       </Flex>
-    </Flex>
+      {dagView === "grid" && (
+        <Flex color="fg.muted" justifyContent="flex-end" mt={1}>
+          <Tooltip
+            content={
+              <Box>
+                <Text>{translate("dag:navigation.navigation", { arrow: "↑↓←→" 
})}</Text>
+                <Text>{translate("dag:navigation.toggleGroup")}</Text>
+              </Box>
+            }
+          >
+            <LuKeyboard />
+          </Tooltip>
+        </Flex>
+      )}
+    </Box>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/ToggleGroups.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/ToggleGroups.tsx
index 756a3298a9b..c55446c1c56 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/ToggleGroups.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/ToggleGroups.tsx
@@ -46,14 +46,13 @@ export const ToggleGroups = (props: ButtonGroupProps) => {
   const collapseLabel = translate("dag:taskGroups.collapseAll");
 
   return (
-    <ButtonGroup attached size="sm" variant="surface" {...props}>
+    <ButtonGroup attached size="sm" variant="outline" {...props}>
       <IconButton
         aria-label={expandLabel}
         disabled={isExpandDisabled}
         onClick={onExpand}
         size="sm"
         title={expandLabel}
-        variant="surface"
       >
         <MdExpand />
       </IconButton>
@@ -63,7 +62,6 @@ export const ToggleGroups = (props: ButtonGroupProps) => {
         onClick={onCollapse}
         size="sm"
         title={collapseLabel}
-        variant="surface"
       >
         <MdCompress />
       </IconButton>


Reply via email to