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

kaxilnaik pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 213c5fe9e73449d7a037a0babe5469d6c79a5da3
Author: Guan Ming(Wesley) Chiu <[email protected]>
AuthorDate: Wed Sep 17 02:07:24 2025 +0800

    Add hover synchronization between Grid and Gantt chart (#55611)
    
    * Add crosshair hover effect between gantt and grid view
    
    * Declare util function outside of the components
    
    (cherry picked from commit 8d772e0062ba67f9ec5ae7215e065f6ba8f22c0b)
---
 .../src/airflow/ui/src/context/hover/Context.ts    |  26 ++
 .../airflow/ui/src/context/hover/HoverProvider.tsx |  36 +++
 .../src/airflow/ui/src/context/hover/index.ts      |  21 ++
 .../src/airflow/ui/src/context/hover/useHover.ts   |  31 +++
 .../ui/src/layouts/Details/DetailsLayout.tsx       | 307 +++++++++++----------
 .../airflow/ui/src/layouts/Details/Gantt/Gantt.tsx |  54 +++-
 .../airflow/ui/src/layouts/Details/Gantt/utils.ts  |  72 ++++-
 .../airflow/ui/src/layouts/Details/Grid/GridTI.tsx |  42 +--
 .../ui/src/layouts/Details/Grid/TaskNames.tsx      |  24 +-
 9 files changed, 424 insertions(+), 189 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/context/hover/Context.ts 
b/airflow-core/src/airflow/ui/src/context/hover/Context.ts
new file mode 100644
index 00000000000..e834b371702
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/context/hover/Context.ts
@@ -0,0 +1,26 @@
+/*!
+ * 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 { createContext } from "react";
+
+export type HoverContextType = {
+  hoveredTaskId: string | undefined;
+  setHoveredTaskId: (taskId: string | undefined) => void;
+};
+
+export const HoverContext = createContext<HoverContextType | 
undefined>(undefined);
diff --git a/airflow-core/src/airflow/ui/src/context/hover/HoverProvider.tsx 
b/airflow-core/src/airflow/ui/src/context/hover/HoverProvider.tsx
new file mode 100644
index 00000000000..e125ce28965
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/context/hover/HoverProvider.tsx
@@ -0,0 +1,36 @@
+/*!
+ * 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 type { PropsWithChildren } from "react";
+import { useState, useMemo } from "react";
+
+import { HoverContext } from "./Context";
+
+export const HoverProvider = ({ children }: PropsWithChildren) => {
+  const [hoveredTaskId, setHoveredTaskId] = useState<string | 
undefined>(undefined);
+
+  const value = useMemo(
+    () => ({
+      hoveredTaskId,
+      setHoveredTaskId,
+    }),
+    [hoveredTaskId],
+  );
+
+  return <HoverContext.Provider 
value={value}>{children}</HoverContext.Provider>;
+};
diff --git a/airflow-core/src/airflow/ui/src/context/hover/index.ts 
b/airflow-core/src/airflow/ui/src/context/hover/index.ts
new file mode 100644
index 00000000000..a1f52fc6f64
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/context/hover/index.ts
@@ -0,0 +1,21 @@
+/*!
+ * 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.
+ */
+export { HoverProvider } from "./HoverProvider";
+export { useHover } from "./useHover";
+export type { HoverContextType } from "./Context";
diff --git a/airflow-core/src/airflow/ui/src/context/hover/useHover.ts 
b/airflow-core/src/airflow/ui/src/context/hover/useHover.ts
new file mode 100644
index 00000000000..541d4306d9d
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/context/hover/useHover.ts
@@ -0,0 +1,31 @@
+/*!
+ * 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 { useContext } from "react";
+
+import { HoverContext } from "./Context";
+
+export const useHover = () => {
+  const context = useContext(HoverContext);
+
+  if (context === undefined) {
+    throw new Error("useHover must be used within a HoverProvider");
+  }
+
+  return context;
+};
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 77910c28326..c19d621ba9a 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
@@ -37,6 +37,7 @@ import { Toaster } from "src/components/ui";
 import ActionButton from "src/components/ui/ActionButton";
 import { DAGWarningsModal } from "src/components/ui/DagWarningsModal";
 import { Tooltip } from "src/components/ui/Tooltip";
+import { HoverProvider } from "src/context/hover";
 import { OpenGroupsProvider } from "src/context/openGroups";
 
 import { DagBreadcrumb } from "./DagBreadcrumb";
@@ -78,164 +79,166 @@ export const DetailsLayout = ({ children, error, 
isLoading, tabs }: Props) => {
   const direction = i18n.dir();
 
   return (
-    <OpenGroupsProvider dagId={dagId}>
-      <HStack justifyContent="space-between" mb={2}>
-        <DagBreadcrumb />
-        <Flex gap={1}>
-          <SearchDagsButton />
-          {dag === undefined ? undefined : (
-            <TriggerDAGButton
-              dagDisplayName={dag.dag_display_name}
-              dagId={dag.dag_id}
-              isPaused={dag.is_paused}
-            />
-          )}
-        </Flex>
-      </HStack>
-      <Toaster />
-      <BackfillBanner dagId={dagId} />
-      <Box flex={1} minH={0}>
-        {isRightPanelCollapsed ? (
-          <Tooltip content={translate("common:showDetailsPanel")}>
-            <IconButton
-              aria-label={translate("common:showDetailsPanel")}
-              bg="fg.subtle"
-              borderRadius={direction === "ltr" ? "100% 0 0 100%" : "0 100% 
100% 0"}
-              boxShadow="md"
-              left={direction === "rtl" ? "-5px" : undefined}
-              onClick={() => setIsRightPanelCollapsed(false)}
-              position="absolute"
-              right={direction === "ltr" ? "-5px" : undefined}
-              size="2xs"
-              top="50%"
-              zIndex={10}
-            >
-              {direction === "ltr" ? <FaChevronLeft /> : <FaChevronRight />}
-            </IconButton>
-          </Tooltip>
-        ) : undefined}
-        <PanelGroup
-          autoSaveId={`${dagView}-${direction}`}
-          dir={direction}
-          direction="horizontal"
-          key={`${dagView}-${direction}`}
-          ref={panelGroupRef}
-        >
-          <Panel
-            defaultSize={dagView === "graph" ? 70 : 20}
-            id="main-panel"
-            minSize={showGantt && dagView === "grid" && Boolean(runId) ? 35 : 
6}
-            order={1}
-          >
-            <Box height="100%" marginInlineEnd={2} overflowY="auto" 
position="relative">
-              <PanelButtons
-                dagView={dagView}
-                limit={limit}
-                panelGroupRef={panelGroupRef}
-                runTypeFilter={runTypeFilter}
-                setDagView={setDagView}
-                setLimit={setLimit}
-                setRunTypeFilter={setRunTypeFilter}
-                setShowGantt={setShowGantt}
-                setTriggeringUserFilter={setTriggeringUserFilter}
-                showGantt={showGantt}
-                triggeringUserFilter={triggeringUserFilter}
+    <HoverProvider>
+      <OpenGroupsProvider dagId={dagId}>
+        <HStack justifyContent="space-between" mb={2}>
+          <DagBreadcrumb />
+          <Flex gap={1}>
+            <SearchDagsButton />
+            {dag === undefined ? undefined : (
+              <TriggerDAGButton
+                dagDisplayName={dag.dag_display_name}
+                dagId={dag.dag_id}
+                isPaused={dag.is_paused}
               />
-              {dagView === "graph" ? (
-                <Graph />
-              ) : (
-                <HStack gap={0}>
-                  <Grid
-                    limit={limit}
-                    runType={runTypeFilter}
-                    showGantt={Boolean(runId) && showGantt}
-                    triggeringUser={triggeringUserFilter}
-                  />
-                  {showGantt ? <Gantt limit={limit} /> : undefined}
-                </HStack>
-              )}
-            </Box>
-          </Panel>
-          {!isRightPanelCollapsed && (
-            <>
-              <PanelResizeHandle
-                className="resize-handle"
-                onDragging={(isDragging) => {
-                  if (!isDragging) {
-                    const zoom = getZoom();
-
-                    void fitView({ maxZoom: zoom, minZoom: zoom });
-                  }
-                }}
+            )}
+          </Flex>
+        </HStack>
+        <Toaster />
+        <BackfillBanner dagId={dagId} />
+        <Box flex={1} minH={0}>
+          {isRightPanelCollapsed ? (
+            <Tooltip content={translate("common:showDetailsPanel")}>
+              <IconButton
+                aria-label={translate("common:showDetailsPanel")}
+                bg="fg.subtle"
+                borderRadius={direction === "ltr" ? "100% 0 0 100%" : "0 100% 
100% 0"}
+                boxShadow="md"
+                left={direction === "rtl" ? "-5px" : undefined}
+                onClick={() => setIsRightPanelCollapsed(false)}
+                position="absolute"
+                right={direction === "ltr" ? "-5px" : undefined}
+                size="2xs"
+                top="50%"
+                zIndex={10}
               >
-                <Box
-                  alignItems="center"
-                  bg="border.emphasized"
-                  cursor="col-resize"
-                  display="flex"
-                  h="100%"
-                  justifyContent="center"
-                  position="relative"
-                  w={0.5}
-                  // onClick={(e) => console.log(e)}
+                {direction === "ltr" ? <FaChevronLeft /> : <FaChevronRight />}
+              </IconButton>
+            </Tooltip>
+          ) : undefined}
+          <PanelGroup
+            autoSaveId={`${dagView}-${direction}`}
+            dir={direction}
+            direction="horizontal"
+            key={`${dagView}-${direction}`}
+            ref={panelGroupRef}
+          >
+            <Panel
+              defaultSize={dagView === "graph" ? 70 : 20}
+              id="main-panel"
+              minSize={showGantt && dagView === "grid" && Boolean(runId) ? 35 
: 6}
+              order={1}
+            >
+              <Box height="100%" marginInlineEnd={2} overflowY="auto" 
position="relative">
+                <PanelButtons
+                  dagView={dagView}
+                  limit={limit}
+                  panelGroupRef={panelGroupRef}
+                  runTypeFilter={runTypeFilter}
+                  setDagView={setDagView}
+                  setLimit={setLimit}
+                  setRunTypeFilter={setRunTypeFilter}
+                  setShowGantt={setShowGantt}
+                  setTriggeringUserFilter={setTriggeringUserFilter}
+                  showGantt={showGantt}
+                  triggeringUserFilter={triggeringUserFilter}
                 />
-              </PanelResizeHandle>
+                {dagView === "graph" ? (
+                  <Graph />
+                ) : (
+                  <HStack gap={0}>
+                    <Grid
+                      limit={limit}
+                      runType={runTypeFilter}
+                      showGantt={Boolean(runId) && showGantt}
+                      triggeringUser={triggeringUserFilter}
+                    />
+                    {showGantt ? <Gantt limit={limit} /> : undefined}
+                  </HStack>
+                )}
+              </Box>
+            </Panel>
+            {!isRightPanelCollapsed && (
+              <>
+                <PanelResizeHandle
+                  className="resize-handle"
+                  onDragging={(isDragging) => {
+                    if (!isDragging) {
+                      const zoom = getZoom();
+
+                      void fitView({ maxZoom: zoom, minZoom: zoom });
+                    }
+                  }}
+                >
+                  <Box
+                    alignItems="center"
+                    bg="border.emphasized"
+                    cursor="col-resize"
+                    display="flex"
+                    h="100%"
+                    justifyContent="center"
+                    position="relative"
+                    w={0.5}
+                    // onClick={(e) => console.log(e)}
+                  />
+                </PanelResizeHandle>
 
-              {/* Collapse button positioned next to the resize handle */}
+                {/* 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="fg.subtle"
-                      borderRadius={direction === "ltr" ? "0 100% 100% 0" : 
"100% 0 0 100%"}
-                      boxShadow="md"
-                      left={direction === "ltr" ? "-5px" : undefined}
-                      onClick={() => setIsRightPanelCollapsed(true)}
-                      position="absolute"
-                      right={direction === "rtl" ? "-5px" : undefined}
-                      size="2xs"
-                      top="50%"
-                      zIndex={2}
-                    >
-                      {direction === "ltr" ? <FaChevronRight /> : 
<FaChevronLeft />}
-                    </IconButton>
-                  </Tooltip>
-                  {children}
-                  {Boolean(error) || (warningData?.dag_warnings.length ?? 0) > 
0 ? (
-                    <>
-                      <ActionButton
-                        actionName={translate("common:dagWarnings")}
-                        colorPalette={Boolean(error) ? "red" : "orange"}
-                        icon={<LuFileWarning />}
-                        margin="2"
-                        marginBottom="-1"
-                        onClick={onOpen}
-                        rounded="full"
-                        text={String(warningData?.total_entries ?? 0 + 
Number(error))}
-                        variant="solid"
-                      />
+                <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="fg.subtle"
+                        borderRadius={direction === "ltr" ? "0 100% 100% 0" : 
"100% 0 0 100%"}
+                        boxShadow="md"
+                        left={direction === "ltr" ? "-5px" : undefined}
+                        onClick={() => setIsRightPanelCollapsed(true)}
+                        position="absolute"
+                        right={direction === "rtl" ? "-5px" : undefined}
+                        size="2xs"
+                        top="50%"
+                        zIndex={2}
+                      >
+                        {direction === "ltr" ? <FaChevronRight /> : 
<FaChevronLeft />}
+                      </IconButton>
+                    </Tooltip>
+                    {children}
+                    {Boolean(error) || (warningData?.dag_warnings.length ?? 0) 
> 0 ? (
+                      <>
+                        <ActionButton
+                          actionName={translate("common:dagWarnings")}
+                          colorPalette={Boolean(error) ? "red" : "orange"}
+                          icon={<LuFileWarning />}
+                          margin="2"
+                          marginBottom="-1"
+                          onClick={onOpen}
+                          rounded="full"
+                          text={String(warningData?.total_entries ?? 0 + 
Number(error))}
+                          variant="solid"
+                        />
 
-                      <DAGWarningsModal
-                        error={error}
-                        onClose={onClose}
-                        open={open}
-                        warnings={warningData?.dag_warnings}
-                      />
-                    </>
-                  ) : undefined}
-                  <ProgressBar size="xs" visibility={isLoading ? "visible" : 
"hidden"} />
-                  <NavTabs tabs={tabs} />
-                  <Box flexGrow={1} overflow="auto" px={2}>
-                    <Outlet />
+                        <DAGWarningsModal
+                          error={error}
+                          onClose={onClose}
+                          open={open}
+                          warnings={warningData?.dag_warnings}
+                        />
+                      </>
+                    ) : undefined}
+                    <ProgressBar size="xs" visibility={isLoading ? "visible" : 
"hidden"} />
+                    <NavTabs tabs={tabs} />
+                    <Box flexGrow={1} overflow="auto" px={2}>
+                      <Outlet />
+                    </Box>
                   </Box>
-                </Box>
-              </Panel>
-            </>
-          )}
-        </PanelGroup>
-      </Box>
-    </OpenGroupsProvider>
+                </Panel>
+              </>
+            )}
+          </PanelGroup>
+        </Box>
+      </OpenGroupsProvider>
+    </HoverProvider>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
index 444cd6c9ce8..d06aa269033 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
@@ -34,13 +34,14 @@ import "chart.js/auto";
 import "chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm";
 import annotationPlugin from "chartjs-plugin-annotation";
 import dayjs from "dayjs";
-import { useMemo, useRef, useDeferredValue } from "react";
+import { useMemo, useDeferredValue } from "react";
 import { Bar } from "react-chartjs-2";
 import { useTranslation } from "react-i18next";
 import { useParams, useNavigate, useLocation } from "react-router-dom";
 
 import { useTaskInstanceServiceGetTaskInstances } from "openapi/queries";
 import { useColorMode } from "src/context/colorMode";
+import { useHover } from "src/context/hover";
 import { useOpenGroups } from "src/context/openGroups";
 import { useTimezone } from "src/context/timezone";
 import { flattenNodes } from "src/layouts/Details/Grid/utils";
@@ -51,7 +52,7 @@ import { getComputedCSSVariableValue } from "src/theme";
 import { isStatePending, useAutoRefresh } from "src/utils";
 import { DEFAULT_DATETIME_FORMAT_WITH_TZ, formatDate } from 
"src/utils/datetimeUtils";
 
-import { createHandleBarClick, createChartOptions } from "./utils";
+import { createHandleBarClick, createHandleBarHover, createChartOptions } from 
"./utils";
 
 ChartJS.register(
   CategoryScale,
@@ -82,18 +83,21 @@ export const Gantt = ({ limit }: Props) => {
   const { t: translate } = useTranslation("common");
   const { selectedTimezone } = useTimezone();
   const { colorMode } = useColorMode();
+  const { hoveredTaskId, setHoveredTaskId } = useHover();
   const navigate = useNavigate();
   const location = useLocation();
-  const ref = useRef();
 
-  const [lightGridColor, darkGridColor, lightSelectedColor, darkSelectedColor] 
= useToken("colors", [
-    "gray.200",
-    "gray.800",
-    "blue.200",
-    "blue.800",
-  ]);
+  const [
+    lightGridColor,
+    darkGridColor,
+    lightSelectedColor,
+    darkSelectedColor,
+    lightHoverColor,
+    darkHoverColor,
+  ] = useToken("colors", ["gray.200", "gray.800", "blue.200", "blue.800", 
"blue.100", "blue.900"]);
   const gridColor = colorMode === "light" ? lightGridColor : darkGridColor;
   const selectedItemColor = colorMode === "light" ? lightSelectedColor : 
darkSelectedColor;
+  const hoveredItemColor = colorMode === "light" ? lightHoverColor : 
darkHoverColor;
 
   const { data: gridRuns, isLoading: runsLoading } = useGridRuns({ limit });
   const { data: dagStructure, isLoading: structureLoading } = 
useGridStructure({ limit });
@@ -218,12 +222,20 @@ export const Gantt = ({ limit }: Props) => {
     [data, dagId, runId, navigate, location],
   );
 
+  const handleBarHover = useMemo(
+    () => createHandleBarHover(data, setHoveredTaskId),
+    [data, setHoveredTaskId],
+  );
+
   const chartOptions = useMemo(
     () =>
       createChartOptions({
         data,
         gridColor,
         handleBarClick,
+        handleBarHover,
+        hoveredId: hoveredTaskId,
+        hoveredItemColor,
         selectedId,
         selectedItemColor,
         selectedRun,
@@ -232,6 +244,8 @@ export const Gantt = ({ limit }: Props) => {
       }),
     [
       data,
+      hoveredTaskId,
+      hoveredItemColor,
       selectedId,
       selectedItemColor,
       gridColor,
@@ -239,6 +253,7 @@ export const Gantt = ({ limit }: Props) => {
       selectedTimezone,
       translate,
       handleBarClick,
+      handleBarHover,
     ],
   );
 
@@ -246,12 +261,29 @@ export const Gantt = ({ limit }: Props) => {
     return undefined;
   }
 
+  const handleChartMouseLeave = () => {
+    setHoveredTaskId(undefined);
+
+    // Clear all hover styles when mouse leaves the chart area
+    const allTasks = document.querySelectorAll<HTMLDivElement>('[id*="-"]');
+
+    allTasks.forEach((task) => {
+      task.style.backgroundColor = "";
+    });
+  };
+
   return (
-    <Box height={`${fixedHeight}px`} minW="250px" ml={-2} mt={36} w="100%">
+    <Box
+      height={`${fixedHeight}px`}
+      minW="250px"
+      ml={-2}
+      mt={36}
+      onMouseLeave={handleChartMouseLeave}
+      w="100%"
+    >
       <Bar
         data={chartData}
         options={chartOptions}
-        ref={ref}
         style={{
           paddingTop: flatNodes.length === 1 ? 15 : 1.5,
         }}
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.ts 
b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.ts
index df93b963910..d3cab195c19 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.ts
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.ts
@@ -47,6 +47,9 @@ type ChartOptionsParams = {
   data: Array<GanttDataItem>;
   gridColor?: string;
   handleBarClick: (event: ChartEvent, elements: Array<ActiveElement>) => void;
+  handleBarHover: (event: ChartEvent, elements: Array<ActiveElement>) => void;
+  hoveredId?: string | null;
+  hoveredItemColor?: string;
   selectedId?: string;
   selectedItemColor?: string;
   selectedRun?: GridRunsResponse;
@@ -83,10 +86,54 @@ export const createHandleBarClick =
     }
   };
 
+export const createHandleBarHover = (
+  data: Array<GanttDataItem>,
+  setHoveredTaskId: (taskId: string | undefined) => void,
+) => {
+  let lastHoveredTaskId: string | undefined = undefined;
+
+  return (_: ChartEvent, elements: Array<ActiveElement>) => {
+    // Clear previous hover styles
+    if (lastHoveredTaskId !== undefined) {
+      const previousTasks = document.querySelectorAll<HTMLDivElement>(
+        `#${lastHoveredTaskId.replaceAll(".", "-")}`,
+      );
+
+      previousTasks.forEach((task) => {
+        task.style.backgroundColor = "";
+      });
+    }
+
+    if (elements.length > 0 && elements[0] && elements[0].index < data.length) 
{
+      const hoveredData = data[elements[0].index];
+
+      if (hoveredData?.taskId !== undefined) {
+        lastHoveredTaskId = hoveredData.taskId;
+        setHoveredTaskId(hoveredData.taskId);
+
+        // Apply new hover styles
+        const tasks = document.querySelectorAll<HTMLDivElement>(
+          `#${hoveredData.taskId.replaceAll(".", "-")}`,
+        );
+
+        tasks.forEach((task) => {
+          task.style.backgroundColor = "var(--chakra-colors-info-subtle)";
+        });
+      }
+    } else {
+      lastHoveredTaskId = undefined;
+      setHoveredTaskId(undefined);
+    }
+  };
+};
+
 export const createChartOptions = ({
   data,
   gridColor,
   handleBarClick,
+  handleBarHover,
+  hoveredId,
+  hoveredItemColor,
   selectedId,
   selectedItemColor,
   selectedRun,
@@ -112,11 +159,14 @@ export const createChartOptions = ({
       if (target) {
         target.style.cursor = elements.length > 0 ? "pointer" : "default";
       }
+
+      handleBarHover(event, elements);
     },
     plugins: {
       annotation: {
-        annotations:
-          selectedId === undefined || selectedId === ""
+        annotations: [
+          // Selected task annotation
+          ...(selectedId === undefined || selectedId === "" || hoveredId === 
selectedId
             ? []
             : [
                 {
@@ -129,7 +179,23 @@ export const createChartOptions = ({
                   yMax: data.findIndex((dataItem) => dataItem.y === 
selectedId) + 0.5,
                   yMin: data.findIndex((dataItem) => dataItem.y === 
selectedId) - 0.5,
                 },
-              ],
+              ]),
+          // Hovered task annotation
+          ...(hoveredId === null || hoveredId === undefined
+            ? []
+            : [
+                {
+                  backgroundColor: hoveredItemColor,
+                  borderWidth: 0,
+                  drawTime: "beforeDatasetsDraw" as const,
+                  type: "box" as const,
+                  xMax: "max" as const,
+                  xMin: "min" as const,
+                  yMax: data.findIndex((dataItem) => dataItem.y === hoveredId) 
+ 0.5,
+                  yMin: data.findIndex((dataItem) => dataItem.y === hoveredId) 
- 0.5,
+                },
+              ]),
+        ],
       },
       legend: {
         display: false,
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 817f6733528..645728236df 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
@@ -26,8 +26,30 @@ 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";
+import { type HoverContextType, useHover } from "src/context/hover";
 import { buildTaskInstanceUrl } from "src/utils/links";
 
+const handleMouseEnter =
+  (setHoveredTaskId: HoverContextType["setHoveredTaskId"]) => (event: 
MouseEvent<HTMLDivElement>) => {
+    const tasks = 
document.querySelectorAll<HTMLDivElement>(`#${event.currentTarget.id}`);
+
+    tasks.forEach((task) => {
+      task.style.backgroundColor = "var(--chakra-colors-info-subtle)";
+    });
+
+    setHoveredTaskId(event.currentTarget.id.replaceAll("-", "."));
+  };
+
+const handleMouseLeave = (taskId: string, setHoveredTaskId: 
HoverContextType["setHoveredTaskId"]) => () => {
+  const tasks = 
document.querySelectorAll<HTMLDivElement>(`#${taskId.replaceAll(".", "-")}`);
+
+  tasks.forEach((task) => {
+    task.style.backgroundColor = "";
+  });
+
+  setHoveredTaskId(undefined);
+};
+
 type Props = {
   readonly dagId: string;
   readonly instance: LightGridTaskInstanceSummary;
@@ -40,27 +62,15 @@ type Props = {
   readonly taskId: string;
 };
 
-const onMouseEnter = (event: MouseEvent<HTMLDivElement>) => {
-  const tasks = 
document.querySelectorAll<HTMLDivElement>(`#${event.currentTarget.id}`);
-
-  tasks.forEach((task) => {
-    task.style.backgroundColor = "var(--chakra-colors-brand-subtle)";
-  });
-};
-
-const onMouseLeave = (event: MouseEvent<HTMLDivElement>) => {
-  const tasks = 
document.querySelectorAll<HTMLDivElement>(`#${event.currentTarget.id}`);
-
-  tasks.forEach((task) => {
-    task.style.backgroundColor = "";
-  });
-};
-
 const Instance = ({ dagId, instance, isGroup, isMapped, onClick, runId, 
search, taskId }: Props) => {
+  const { setHoveredTaskId } = useHover();
   const { groupId: selectedGroupId, taskId: selectedTaskId } = useParams();
   const { t: translate } = useTranslation();
   const location = useLocation();
 
+  const onMouseEnter = handleMouseEnter(setHoveredTaskId);
+  const onMouseLeave = handleMouseLeave(taskId, setHoveredTaskId);
+
   const getTaskUrl = useCallback(
     () =>
       buildTaskInstanceUrl({
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskNames.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskNames.tsx
index 2e2ba611eca..05bf16c83c9 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskNames.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskNames.tsx
@@ -23,6 +23,7 @@ import { FiChevronUp } from "react-icons/fi";
 import { Link as RouterLink, useParams, useSearchParams } from 
"react-router-dom";
 
 import { TaskName } from "src/components/TaskName";
+import { type HoverContextType, useHover } from "src/context/hover";
 import { useOpenGroups } from "src/context/openGroups";
 
 import type { GridTask } from "./utils";
@@ -33,26 +34,35 @@ type Props = {
   onRowClick?: () => void;
 };
 
-const onMouseEnter = (event: MouseEvent<HTMLDivElement>) => {
+const indent = (depth: number) => `${depth * 0.75 + 0.5}rem`;
+
+const onMouseEnter = (
+  event: MouseEvent<HTMLDivElement>,
+  nodeId: string,
+  setHoveredTaskId: HoverContextType["setHoveredTaskId"],
+) => {
   const tasks = 
document.querySelectorAll<HTMLDivElement>(`#${event.currentTarget.id}`);
 
   tasks.forEach((task) => {
     task.style.backgroundColor = "var(--chakra-colors-info-subtle)";
   });
+
+  setHoveredTaskId(nodeId);
 };
 
-const onMouseLeave = (event: MouseEvent<HTMLDivElement>) => {
-  const tasks = 
document.querySelectorAll<HTMLDivElement>(`#${event.currentTarget.id}`);
+const onMouseLeave = (nodeId: string, setHoveredTaskId: 
HoverContextType["setHoveredTaskId"]) => {
+  const tasks = 
document.querySelectorAll<HTMLDivElement>(`#${nodeId.replaceAll(".", "-")}`);
 
   tasks.forEach((task) => {
     task.style.backgroundColor = "";
   });
-};
 
-const indent = (depth: number) => `${depth * 0.75 + 0.5}rem`;
+  setHoveredTaskId(undefined);
+};
 
 export const TaskNames = ({ nodes, onRowClick }: Props) => {
   const { t: translate } = useTranslation("dag");
+  const { setHoveredTaskId } = useHover();
   const { toggleGroupId } = useOpenGroups();
   const { dagId = "", groupId, taskId } = useParams();
   const [searchParams] = useSearchParams();
@@ -66,8 +76,8 @@ export const TaskNames = ({ nodes, onRowClick }: Props) => {
       id={node.id.replaceAll(".", "-")}
       key={node.id}
       maxHeight="20px"
-      onMouseEnter={onMouseEnter}
-      onMouseLeave={onMouseLeave}
+      onMouseEnter={(event) => onMouseEnter(event, node.id, setHoveredTaskId)}
+      onMouseLeave={() => onMouseLeave(node.id, setHoveredTaskId)}
       transition="background-color 0.2s"
     >
       {node.isGroup ? (

Reply via email to