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

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


The following commit(s) were added to refs/heads/v3-4-test by this push:
     new 32c12f2bebd [v3-4-test] Open the Dags menu from the Dags nav button 
instead of navigating (#75092) (#75109)
32c12f2bebd is described below

commit 32c12f2bebdaa2b0692c08f8cae21e26210515a9
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Oct 19 18:04:41 2026 -0400

    [v3-4-test] Open the Dags menu from the Dags nav button instead of 
navigating (#75092) (#75109)
    
    The Dags button now heads a menu like Browse and Security, but it was
    still a link. A click navigated to the Dags list and opened the menu
    over the new page at the same time, which reads as a flicker. Treating
    it as a menu trigger makes it behave like its siblings: hover or click
    opens the menu, and the Dags list is one of its entries.
    (cherry picked from commit ab1acec7aa831f77b6c58f76010e5613933c797c)
    
    Co-authored-by: Pierre Jeambrun <[email protected]>
---
 .../airflow/ui/src/layouts/Nav/DagsButton.test.tsx | 36 +++++++++++++++++++---
 .../src/airflow/ui/src/layouts/Nav/DagsButton.tsx  |  4 +--
 .../airflow/ui/src/layouts/Nav/NavButton.test.tsx  | 18 -----------
 .../src/airflow/ui/src/layouts/Nav/NavButton.tsx   | 17 ++--------
 4 files changed, 35 insertions(+), 40 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/DagsButton.test.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/DagsButton.test.tsx
index 381f12de0f3..b1146cd4eb4 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/DagsButton.test.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/DagsButton.test.tsx
@@ -16,23 +16,51 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import type { PropsWithChildren } from "react";
+
 import "@testing-library/jest-dom/vitest";
 import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { MemoryRouter } from "react-router-dom";
 import { describe, expect, it } from "vitest";
 
-import { Wrapper } from "src/utils/Wrapper";
+import { BaseWrapper, Wrapper } from "src/utils/Wrapper";
 
 import { DagsButton } from "./DagsButton";
 
-const getTrigger = () => screen.getByRole("link", { name: "nav.dags" });
+const getTrigger = () => screen.getByRole("button", { name: "nav.dags" });
+
+const wrapperAt = (path: string) => {
+  const wrapper = ({ children }: PropsWithChildren) => (
+    <BaseWrapper>
+      <MemoryRouter initialEntries={[path]}>{children}</MemoryRouter>
+    </BaseWrapper>
+  );
+
+  return wrapper;
+};
 
 describe("DagsButton", () => {
-  it("links straight to the Dags list", () => {
+  it("opens the menu on click instead of navigating", async () => {
     render(<DagsButton authorizedMenuItems={["Dags"]} />, { wrapper: Wrapper 
});
 
-    expect(getTrigger()).toHaveAttribute("href", "/dags");
+    expect(getTrigger()).not.toHaveAttribute("href");
+
+    fireEvent.click(getTrigger());
+
+    await waitFor(() => {
+      expect(screen.getAllByRole("menuitem")).toHaveLength(3);
+    });
   });
 
+  it.each(["/dags", "/dags/example_dag/runs", "/dag_runs", "/task_instances"])(
+    "is highlighted on %s",
+    (path) => {
+      render(<DagsButton authorizedMenuItems={["Dags"]} />, { wrapper: 
wrapperAt(path) });
+
+      expect(getTrigger()).toHaveAttribute("aria-current", "page");
+    },
+  );
+
   it("reveals the dags, dag runs and task instances links on hover", async () 
=> {
     render(<DagsButton authorizedMenuItems={["Dags"]} />, { wrapper: Wrapper 
});
 
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/DagsButton.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/DagsButton.tsx
index 4618285a92c..3f7b7aa5c5d 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/DagsButton.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/DagsButton.tsx
@@ -47,15 +47,13 @@ export const DagsButton = ({ authorizedMenuItems }: { 
readonly authorizedMenuIte
   const { t: translate } = useTranslation();
   const isAuthorized = authorizedMenuItems.includes("Dags");
 
-  // Stays a link to the Dags list: hovering reveals the menu, clicking goes 
straight there.
   const trigger = (
     <NavButton
       data-testid="nav-dags-link"
       disabled={!isAuthorized}
       icon={DagIcon}
-      matchPaths={["dag_runs", "task_instances"]}
       title={translate("nav.dags")}
-      to="/dags"
+      to={links.map(({ href }) => href)}
     />
   );
 
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.test.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.test.tsx
index 696441f218a..37b10be834c 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.test.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.test.tsx
@@ -108,22 +108,4 @@ describe("NavButton", () => {
       expect(link).toHaveAttribute("rel", "noopener noreferrer");
     });
   });
-
-  describe("matchPaths", () => {
-    it("is active on an extra match path even though `to` points elsewhere", 
() => {
-      render(<NavButton icon={FiHome} matchPaths={["dag_runs", 
"task_instances"]} title="Dags" to="dags" />, {
-        wrapper: wrapperAt("/dag_runs"),
-      });
-
-      expect(screen.getByRole("link", { name: "Dags" 
})).toHaveAttribute("aria-current", "page");
-    });
-
-    it("is not active on a route outside both `to` and matchPaths", () => {
-      render(<NavButton icon={FiHome} matchPaths={["dag_runs", 
"task_instances"]} title="Dags" to="dags" />, {
-        wrapper: wrapperAt("/assets"),
-      });
-
-      expect(screen.getByRole("link", { name: "Dags" 
})).not.toHaveAttribute("aria-current");
-    });
-  });
 });
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx
index 8b04f107330..5b1f7d6c2e0 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx
@@ -22,8 +22,6 @@ import { Box, type BoxProps, Button, Icon, type IconProps, 
Link, type ButtonProp
 import type { IconType } from "react-icons";
 import { Link as RouterLink, matchPath, useLocation } from "react-router-dom";
 
-const noMatchPaths: Array<string> = [];
-
 const commonLabelProps: BoxProps = {
   fontSize: "2xs",
   overflow: "hidden",
@@ -36,9 +34,6 @@ const commonLabelProps: BoxProps = {
 type NavButtonProps = {
   readonly icon: ForwardRefExoticComponent<IconProps & 
RefAttributes<SVGSVGElement>> | IconType;
   readonly isExternal?: boolean;
-  // Extra routes that should also mark this button active, on top of `to` 
(e.g. the Dags button
-  // should also highlight for the standalone dag runs and task instances 
routes).
-  readonly matchPaths?: Array<string>;
   readonly pluginIcon?: ReactNode;
   readonly title: string;
   // A single destination renders the button as a link; an array only affects 
isActive matching
@@ -46,18 +41,10 @@ type NavButtonProps = {
   readonly to?: Array<string> | string;
 } & ButtonProps;
 
-export const NavButton = ({
-  icon,
-  isExternal = false,
-  matchPaths = noMatchPaths,
-  pluginIcon,
-  title,
-  to,
-  ...rest
-}: NavButtonProps) => {
+export const NavButton = ({ icon, isExternal = false, pluginIcon, title, to, 
...rest }: NavButtonProps) => {
   const { pathname } = useLocation();
 
-  const activePaths = [...(to === undefined ? [] : Array.isArray(to) ? to : 
[to]), ...matchPaths];
+  const activePaths = to === undefined ? [] : Array.isArray(to) ? to : [to];
   const isActive = activePaths.some((path) => matchPath({ end: path === "/", 
path }, pathname) !== null);
 
   const commonButtonProps: ButtonProps = {

Reply via email to