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
},
}
10 changes: 5 additions & 5 deletions airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,15 @@ export const Nav = () => {
width={16}
zIndex={2}
>
<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: {
transform: "rotate(360deg)",
transition: "transform 0.8s ease-in-out"
}
transition: "transform 0.8s ease-in-out",
},
}}
boxSize={8}
/>
Expand Down
112 changes: 59 additions & 53 deletions airflow-core/src/airflow/ui/src/layouts/Nav/NavButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -31,87 +31,93 @@ 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>
);
}

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

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>
Expand Down
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
4 changes: 2 additions & 2 deletions airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.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 { 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";
Expand Down Expand Up @@ -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) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
Expand Down