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

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

commit 577fbed7f9b50d8c97f164bad1c052b8d0634286
Author: Ryan Hamilton <[email protected]>
AuthorDate: Thu Oct 30 10:02:15 2025 -0400

    Add settings to auto-apply linting, fix linting errors (#57510)
---
 .gitignore                                         |   2 -
 .vscode/extensions.json                            |   5 +
 .vscode/settings.json                              |  38 +++++++
 .../src/airflow/ui/src/layouts/Nav/Nav.tsx         |   6 +-
 .../src/airflow/ui/src/layouts/Nav/NavButton.tsx   | 112 +++++++++++----------
 .../airflow/ui/src/layouts/Nav/PluginMenuItem.tsx  |  19 ++--
 .../src/airflow/ui/src/layouts/Nav/PluginMenus.tsx |   4 +-
 .../ui/src/layouts/Nav/UserSettingsButton.tsx      |   4 +-
 8 files changed, 121 insertions(+), 69 deletions(-)

diff --git a/.gitignore b/.gitignore
index e7fb2a5403d..1528de626bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -171,8 +171,6 @@ npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
 pnpm-debug.log*
-.vscode/*
-!.vscode/extensions.json
 /.vite/
 # Exclude the ui .vite dir
 airflow-core/src/airflow/ui/.vite/
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000000..0e6d5fd64e4
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+  "recommendations": [
+    "esbenp.prettier-vscode",
+  ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000000..aeae611e1fe
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,38 @@
+{
+  "[javascript]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode",
+    "editor.formatOnSave": true,
+    "editor.codeActionsOnSave": {
+      "source.fixAll.eslint": "explicit"
+    }
+  },
+  "[javascriptreact]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode",
+    "editor.formatOnSave": true,
+    "editor.codeActionsOnSave": {
+      "source.fixAll.eslint": "explicit"
+    }
+  },
+  "[typescript]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode",
+    "editor.formatOnSave": true,
+    "editor.codeActionsOnSave": {
+      "source.fixAll.eslint": "explicit"
+    }
+  },
+  "[typescriptreact]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode",
+    "editor.formatOnSave": true,
+    "editor.codeActionsOnSave": {
+      "source.fixAll.eslint": "explicit"
+    }
+  },
+  "[json]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode",
+    "editor.formatOnSave": true
+  },
+  "[jsonc]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode",
+    "editor.formatOnSave": true
+  },
+}
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
index 5fa459b7959..2203df73880 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
@@ -145,9 +145,9 @@ export const Nav = () => {
       width={16}
       zIndex="docked"
     >
-      <Flex alignItems="center" flexDir="column" width="100%" gap={1}>
-        <Box asChild boxSize={14} display="flex" alignItems="center" 
justifyContent="center">
-          <Link to="/" title={translate("nav.home")}>
+      <Flex alignItems="center" flexDir="column" gap={1} width="100%">
+        <Box alignItems="center" asChild boxSize={14} display="flex" 
justifyContent="center">
+          <Link title={translate("nav.home")} to="/">
             <AirflowPin
               _motionSafe={{
                 _hover: {
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 3ebea71bef1..eaa13f57f1b 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx
@@ -17,8 +17,8 @@
  * under the License.
  */
 import { Box, type BoxProps, Button, Icon, type IconProps, Link, type 
ButtonProps } from "@chakra-ui/react";
-import { useMemo, type ForwardRefExoticComponent, type RefAttributes } from 
"react";
-import { IconType } from "react-icons";
+import { type ReactNode, useMemo, type ForwardRefExoticComponent, type 
RefAttributes } from "react";
+import type { IconType } from "react-icons";
 import { Link as RouterLink, useMatch } from "react-router-dom";
 
 const commonLabelProps: BoxProps = {
@@ -31,63 +31,73 @@ const commonLabelProps: BoxProps = {
 };
 
 type NavButtonProps = {
-  readonly icon: IconType | ForwardRefExoticComponent<IconProps & 
RefAttributes<SVGSVGElement>>;
+  readonly icon: ForwardRefExoticComponent<IconProps & 
RefAttributes<SVGSVGElement>> | IconType;
   readonly isExternal?: boolean;
+  readonly pluginIcon?: ReactNode;
   readonly title: string;
   readonly to?: string;
 } & ButtonProps;
 
-export const NavButton = ({ icon, isExternal = false, title, to, ...rest }: 
NavButtonProps) => {
+export const NavButton = ({ icon, isExternal = false, pluginIcon, title, to, 
...rest }: NavButtonProps) => {
   // Use useMatch to determine if the current route matches the button's 
destination
   // This provides the same functionality as NavLink's isActive prop
   // Only applies to buttons with a to prop (but needs to be before any return 
statements)
-  const match = to ? useMatch({
-    path: to,
-    end: to === "/" // Only exact match for root path
-  }) : undefined;
+  const match = useMatch({
+    end: to === "/", // Only exact match for root path
+    path: to ?? "",
+  });
   // Only applies to buttons with a to prop
-  const isActive = Boolean(match);
+  const isActive = Boolean(to) ? Boolean(match) : false;
 
-  const commonButtonProps = useMemo<ButtonProps>(() => ({
-    _expanded: isActive ? undefined : {
-      bg: "brand.emphasized", // Even darker for better light mode contrast
-      color: "fg",
-    },
-    _focus: isActive ? undefined : {
-      color: "fg",
-    },
-    _hover: isActive ? undefined : {
-      bg: "brand.emphasized", // Even darker for better light mode contrast
-      color: "fg",
-      _active: {
-        bg: "brand.solid",
-        color: "white",
-      },
-    },
-    alignItems: "center",
-    bg: isActive ? "brand.solid" : undefined,
-    borderRadius: "md",
-    borderWidth: 0,
-    boxSize: 14,
-    color: isActive ? "white" : "fg.muted",
-    colorPalette: "brand",
-    cursor: "pointer",
-    flexDir: "column",
-    gap: 0,
-    overflow: "hidden",
-    padding: 0,
-    textDecoration: "none",
-    title,
-    transition: "background-color 0.2s ease, color 0.2s ease",
-    variant: "plain",
-    whiteSpace: "wrap",
-    ...rest,
-  }), [isActive, rest, title]);
+  const commonButtonProps = useMemo<ButtonProps>(
+    () => ({
+      _expanded: isActive
+        ? undefined
+        : {
+            bg: "brand.emphasized", // Even darker for better light mode 
contrast
+            color: "fg",
+          },
+      _focus: isActive
+        ? undefined
+        : {
+            color: "fg",
+          },
+      _hover: isActive
+        ? undefined
+        : {
+            _active: {
+              bg: "brand.solid",
+              color: "white",
+            },
+            bg: "brand.emphasized", // Even darker for better light mode 
contrast
+            color: "fg",
+          },
+      alignItems: "center",
+      bg: isActive ? "brand.solid" : undefined,
+      borderRadius: "md",
+      borderWidth: 0,
+      boxSize: 14,
+      color: isActive ? "white" : "fg.muted",
+      colorPalette: "brand",
+      cursor: "pointer",
+      flexDir: "column",
+      gap: 0,
+      overflow: "hidden",
+      padding: 0,
+      textDecoration: "none",
+      title,
+      transition: "background-color 0.2s ease, color 0.2s ease",
+      variant: "plain",
+      whiteSpace: "wrap",
+      ...rest,
+    }),
+    [isActive, rest, title],
+  );
 
   if (to === undefined) {
     return (
       <Button {...commonButtonProps}>
-        <Icon as={icon} boxSize={5} />
+        {pluginIcon ?? <Icon as={icon} boxSize={5} />}
         <Box {...commonLabelProps}>{title}</Box>
       </Button>
     );
@@ -95,9 +105,9 @@ export const NavButton = ({ icon, isExternal = false, title, 
to, ...rest }: NavB
 
   if (isExternal) {
     return (
-      <Link href={to} asChild rel="noopener noreferrer" target="_blank">
+      <Link asChild href={to} rel="noopener noreferrer" target="_blank">
         <Button {...commonButtonProps}>
-          <Icon as={icon} boxSize={5} />
+          {pluginIcon ?? <Icon as={icon} boxSize={5} />}
           <Box {...commonLabelProps}>{title}</Box>
         </Button>
       </Link>
@@ -105,13 +115,9 @@ export const NavButton = ({ icon, isExternal = false, 
title, to, ...rest }: NavB
   }
 
   return (
-    <Button
-      as={Link}
-      asChild
-      {...commonButtonProps}
-    >
+    <Button as={Link} asChild {...commonButtonProps}>
       <RouterLink to={to}>
-        <Icon as={icon} boxSize={5} />
+        {pluginIcon ?? <Icon as={icon} boxSize={5} />}
         <Box {...commonLabelProps}>{title}</Box>
       </RouterLink>
     </Button>
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx
index c7372703237..5d36cf59e10 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Link, Image, Menu } from "@chakra-ui/react";
+import { Link, Image, Menu, Icon, Box } from "@chakra-ui/react";
 import { FiExternalLink } from "react-icons/fi";
 import { LuPlug } from "react-icons/lu";
 import { RiArchiveStackLine } from "react-icons/ri";
@@ -46,11 +46,11 @@ export const PluginMenuItem = ({
   const displayIcon = colorMode === "dark" && typeof iconDarkMode === "string" 
? iconDarkMode : icon;
   const pluginIcon =
     typeof displayIcon === "string" ? (
-      <Image height="20px" mr={topLevel ? 0 : 2} src={displayIcon} 
width="20px" />
+      <Image boxSize={5} src={displayIcon} />
     ) : urlRoute === "legacy-fab-views" ? (
-      <RiArchiveStackLine size="20px" style={{ marginRight: topLevel ? 0 : 
"8px" }} />
+      <Icon as={RiArchiveStackLine} boxSize={5} />
     ) : (
-      <LuPlug size="20px" style={{ marginRight: topLevel ? 0 : "8px" }} />
+      <Icon as={LuPlug} boxSize={5} />
     );
 
   const isExternal = urlRoute === undefined || urlRoute === null;
@@ -58,9 +58,10 @@ export const PluginMenuItem = ({
   if (topLevel) {
     return (
       <NavButton
-        icon={pluginIcon}
+        icon={LuPlug}
         isExternal={isExternal}
         key={name}
+        pluginIcon={pluginIcon}
         title={name}
         to={isExternal ? href : `plugin/${urlRoute}`}
       />
@@ -80,13 +81,15 @@ export const PluginMenuItem = ({
           width="100%"
         >
           {pluginIcon}
-          {name}
-          <FiExternalLink />
+          <Box flex="1">{name}</Box>
+          <Icon as={FiExternalLink} boxSize={4} color="fg.muted" />
         </Link>
       ) : (
         <RouterLink style={{ outline: "none" }} to={`plugin/${urlRoute}`}>
           {pluginIcon}
-          {name}
+          <Box flex="1" ml={2}>
+            {name}
+          </Box>
         </RouterLink>
       )}
     </Menu.Item>
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx
index c8727804890..9ac31f6947f 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box } from "@chakra-ui/react";
+import { Box, Icon } from "@chakra-ui/react";
 import { useTranslation } from "react-i18next";
 import { FiChevronRight } from "react-icons/fi";
 import { LuPlug } from "react-icons/lu";
@@ -63,7 +63,7 @@ export const PluginMenus = ({ navItems }: { readonly 
navItems: Array<NavItemResp
           <Menu.Root key={key} positioning={{ placement: "right" }}>
             <Menu.TriggerItem display="flex" justifyContent="space-between">
               {key}
-              <FiChevronRight />
+              <Icon as={FiChevronRight} boxSize={4} color="fg.muted" />
             </Menu.TriggerItem>
             <Menu.Content>
               {menuButtons.map((navItem) => (
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
index 15ee91b0422..84d8c385fed 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
@@ -121,7 +121,9 @@ export const UserSettingsButton = ({ externalViews }: { 
readonly externalViews:
             value={dagView}
           >
             <Icon as={dagView === "grid" ? MdOutlineAccountTree : FiGrid} 
boxSize={4} />
-            <Box flex="1">{dagView === "grid" ? 
translate("defaultToGraphView") : translate("defaultToGridView")}</Box>
+            <Box flex="1">
+              {dagView === "grid" ? translate("defaultToGraphView") : 
translate("defaultToGridView")}
+            </Box>
           </Menu.Item>
           <TimezoneMenuItem onOpen={onOpenTimezone} />
           {externalViews.map((view) => (

Reply via email to