Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const AdminButton = ({
return (
<Menu.Root positioning={{ placement: "right" }}>
<Menu.Trigger asChild>
<NavButton icon={<FiSettings size={28} />} title={translate("nav.admin")} />
<NavButton icon={FiSettings} title={translate("nav.admin")} />
</Menu.Trigger>
<Menu.Content>
{menuItems}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const BrowseButton = ({
return (
<Menu.Root positioning={{ placement: "right" }}>
<Menu.Trigger asChild>
<NavButton icon={<FiGlobe size={28} />} title={translate("nav.browse")} />
<NavButton icon={FiGlobe} title={translate("nav.browse")} />
</Menu.Trigger>
<Menu.Content>
{menuItems}
Expand Down
13 changes: 7 additions & 6 deletions airflow-core/src/airflow/ui/src/layouts/Nav/DocsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Link } from "@chakra-ui/react";
import { Box, Icon, Link } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { FiBookOpen, FiExternalLink } from "react-icons/fi";

Expand Down Expand Up @@ -61,7 +61,7 @@ export const DocsButton = ({
return (
<Menu.Root positioning={{ placement: "right" }}>
<Menu.Trigger asChild>
<NavButton icon={<FiBookOpen size={28} />} title={translate("nav.docs")} />
<NavButton icon={FiBookOpen} title={translate("nav.docs")} />
</Menu.Trigger>
<Menu.Content>
{links
Expand All @@ -73,17 +73,18 @@ export const DocsButton = ({
href={link.href}
rel="noopener noreferrer"
target="_blank"
textDecoration="none"
>
{translate(`docs.${link.key}`)}
<FiExternalLink />
<Box flex="1">{translate(`docs.${link.key}`)}</Box>
<Icon as={FiExternalLink} boxSize={4} color="fg.muted" />
</Link>
</Menu.Item>
))}
{version === undefined ? undefined : (
<Menu.Item asChild key={version} value={version}>
<Link aria-label={version} href={versionLink} rel="noopener noreferrer" target="_blank">
{version}
<FiExternalLink />
<Box flex="1">{version}</Box>
<Icon as={FiExternalLink} boxSize={4} color="fg.muted" />
</Link>
</Menu.Item>
)}
Expand Down
25 changes: 12 additions & 13 deletions airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { Box, Flex, VStack } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { FiDatabase, FiHome } from "react-icons/fi";
import { NavLink } from "react-router-dom";
import { Link } from "react-router-dom";

import {
useAuthLinksServiceGetAuthMenus,
Expand Down Expand Up @@ -140,36 +140,35 @@ export const Nav = () => {
height="100%"
justifyContent="space-between"
position="fixed"
py={3}
py={1}
top={0}
width={20}
width={16}
zIndex={2}
>
<Flex alignItems="center" flexDir="column" width="100%">
<Box mb={3}>
<NavLink to="/">
<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")}>
<AirflowPin
_motionSafe={{
_hover: {
transform: "rotate(360deg)",
transition: "transform 0.8s ease-in-out",
},
}}
height="35px"
width="35px"
boxSize={8}
/>
</NavLink>
</Link>
</Box>
<NavButton icon={<FiHome size="28px" />} title={translate("nav.home")} to="/" />
<NavButton icon={FiHome} title={translate("nav.home")} to="/" />
<NavButton
disabled={!authLinks?.authorized_menu_items.includes("Dags")}
icon={<DagIcon height="28px" width="28px" />}
icon={DagIcon}
title={translate("nav.dags")}
to="dags"
/>
<NavButton
disabled={!authLinks?.authorized_menu_items.includes("Assets")}
icon={<FiDatabase size="28px" />}
icon={FiDatabase}
title={translate("nav.assets")}
to="assets"
/>
Expand All @@ -184,7 +183,7 @@ export const Nav = () => {
<SecurityButton />
<PluginMenus navItems={navItemsWithLegacy} />
</Flex>
<Flex flexDir="column">
<Flex flexDir="column" gap={1}>
<DocsButton
externalViews={docsItems}
showAPI={authLinks?.authorized_menu_items.includes("Docs")}
Expand Down
140 changes: 91 additions & 49 deletions airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,62 +16,104 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Box, Button, Link, type ButtonProps } from "@chakra-ui/react";
import type { ReactElement } from "react";
import { NavLink } from "react-router-dom";
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 { Link as RouterLink, useMatch } from "react-router-dom";

const styles = {
_active: {
bg: "brand.emphasized",
},
// Fix inverted hover and active colors
_hover: {
bg: "brand.emphasized", // Even darker for better light mode contrast
},
alignItems: "center",
borderRadius: "none",
colorPalette: "brand",
flexDir: "column",
height: 20,
variant: "ghost",
whiteSpace: "wrap",
width: 20,
} satisfies ButtonProps;
const commonLabelProps: BoxProps = {
fontSize: "2xs",
overflow: "hidden",
textAlign: "center",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
width: "full",
};

type NavButtonProps = {
readonly icon: ReactElement;
readonly icon: IconType | ForwardRefExoticComponent<IconProps & RefAttributes<SVGSVGElement>>;
readonly isExternal?: boolean;
readonly title?: string;
readonly title: string;
readonly to?: string;
} & ButtonProps;

export const NavButton = ({ icon, isExternal = false, title, to, ...rest }: NavButtonProps) =>
to === undefined ? (
<Button {...styles} {...rest}>
<Box alignSelf="center">{icon}</Box>
<Box fontSize="xs">{title}</Box>
</Button>
) : isExternal ? (
<Link href={to} px={2} rel="noopener noreferrer" target="_blank">
<Button {...styles} variant="ghost" {...rest}>
<Box alignSelf="center">{icon}</Box>
<Box fontSize="xs">{title}</Box>
export const NavButton = ({ icon, isExternal = false, 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;
// Only applies to buttons with a to prop
const isActive = Boolean(match);

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]);

if (to === undefined) {
return (
<Button {...commonButtonProps}>
<Icon as={icon} boxSize={5} />
<Box {...commonLabelProps}>{title}</Box>
</Button>
</Link>
) : (
<NavLink to={to}>
{({ isActive }: { readonly isActive: boolean }) => (
<Button
{...styles}
_active={isActive ? { bg: "brand.solid" } : { bg: "brand.emphasized" }}
// Override styles for active state to ensure proper colors
_hover={isActive ? { bg: "brand.solid" } : { bg: "brand.emphasized" }}
variant={isActive ? "solid" : "ghost"}
{...rest}
>
<Box alignSelf="center">{icon}</Box>
<Box fontSize="xs">{title}</Box>
);
}

if (isExternal) {
return (
<Link href={to} asChild rel="noopener noreferrer" target="_blank">
<Button {...commonButtonProps}>
<Icon as={icon} boxSize={5} />
<Box {...commonLabelProps}>{title}</Box>
</Button>
)}
</NavLink>
</Link>
);
}

return (
<Button
as={Link}
asChild
{...commonButtonProps}
>
<RouterLink to={to}>
<Icon as={icon} boxSize={5} />
<Box {...commonLabelProps}>{title}</Box>
</RouterLink>
</Button>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const PluginMenus = ({ navItems }: { readonly navItems: Array<NavItemResp
return navItems.length >= 2 ? (
<Menu.Root positioning={{ placement: "right" }}>
<Menu.Trigger>
<NavButton as={Box} icon={<LuPlug />} title={translate("nav.plugins")} />
<NavButton as={Box} icon={LuPlug} title={translate("nav.plugins")} />
</Menu.Trigger>
<Menu.Content>
{buttons.map((navItem) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const SecurityButton = () => {
return (
<Menu.Root positioning={{ placement: "right" }}>
<Menu.Trigger asChild>
<NavButton icon={<FiLock size={28} />} title={translate("nav.security")} />
<NavButton icon={FiLock} title={translate("nav.security")} />
</Menu.Trigger>
<Menu.Content>
{authLinks.extra_menu_items.map(({ text }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Box, Icon } from "@chakra-ui/react";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
Expand Down Expand Up @@ -48,8 +49,8 @@ export const TimezoneMenuItem = ({ onOpen }: { readonly onOpen: () => void }) =>

return (
<Menu.Item onClick={onOpen} value="timezone">
<FiClock size={20} style={{ marginRight: "8px" }} />
{translate("timezone")}: {dayjs(time).tz(selectedTimezone).format("HH:mm z (Z)")}
<Icon as={FiClock} boxSize={4} />
<Box flex="1">{translate("timezone")}: {dayjs(time).tz(selectedTimezone).format("HH:mm z (Z)")}</Box>
</Menu.Item>
);
};
Loading
Loading