Skip to content
Merged
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
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
5 changes: 5 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"esbenp.prettier-vscode",
]
}
38 changes: 38 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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
},
}
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="docked"
>
<Flex alignItems="center" flexDir="column" width="100%">
<Box mb={3}>
<NavLink to="/">
<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: {
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
146 changes: 97 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,110 @@
* 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 { 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 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: ForwardRefExoticComponent<IconProps & RefAttributes<SVGSVGElement>> | IconType;
readonly isExternal?: boolean;
readonly title?: string;
readonly pluginIcon?: ReactNode;
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, 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 = useMatch({
end: to === "/", // Only exact match for root path
path: to ?? "",
});
// Only applies to buttons with a to prop
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
: {
_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}>
{pluginIcon ?? <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 asChild href={to} rel="noopener noreferrer" target="_blank">
<Button {...commonButtonProps}>
{pluginIcon ?? <Icon as={icon} boxSize={5} />}
<Box {...commonLabelProps}>{title}</Box>
</Button>
)}
</NavLink>
</Link>
);
}

return (
<Button as={Link} asChild {...commonButtonProps}>
<RouterLink to={to}>
{pluginIcon ?? <Icon as={icon} boxSize={5} />}
<Box {...commonLabelProps}>{title}</Box>
</RouterLink>
</Button>
);
};
19 changes: 11 additions & 8 deletions airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.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, 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";
Expand Down Expand Up @@ -46,21 +46,22 @@ 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;

if (topLevel) {
return (
<NavButton
icon={pluginIcon}
icon={LuPlug}
isExternal={isExternal}
key={name}
pluginIcon={pluginIcon}
title={name}
to={isExternal ? href : `plugin/${urlRoute}`}
/>
Expand All @@ -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>
Expand Down
Loading
Loading