You've already forked nginx-proxy-manager
mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-06-27 07:41:49 +03:00
Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager
This commit is contained in:
331
frontend/src/components/Navigation/NavigationMenu.tsx
Normal file
331
frontend/src/components/Navigation/NavigationMenu.tsx
Normal file
@ -0,0 +1,331 @@
|
||||
import { FC, useCallback, useMemo, ReactNode } from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Collapse,
|
||||
Flex,
|
||||
forwardRef,
|
||||
HStack,
|
||||
Icon,
|
||||
Link,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Text,
|
||||
Stack,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
Container,
|
||||
useBreakpointValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { intl } from "locale";
|
||||
import {
|
||||
FiHome,
|
||||
FiSettings,
|
||||
FiUser,
|
||||
FiBook,
|
||||
FiLock,
|
||||
FiShield,
|
||||
FiMonitor,
|
||||
FiChevronDown,
|
||||
} from "react-icons/fi";
|
||||
import { Link as RouterLink, useLocation } from "react-router-dom";
|
||||
|
||||
interface NavItem {
|
||||
/** Displayed label */
|
||||
label: string;
|
||||
/** Icon shown before the label */
|
||||
icon: ReactNode;
|
||||
/** Link where to navigate to */
|
||||
to?: string;
|
||||
subItems?: { label: string; to: string }[];
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{
|
||||
label: intl.formatMessage({ id: "dashboard.title" }),
|
||||
icon: <Icon as={FiHome} />,
|
||||
to: "/",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "hosts.title" }),
|
||||
icon: <Icon as={FiMonitor} />,
|
||||
to: "/hosts",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "access-lists.title" }),
|
||||
icon: <Icon as={FiLock} />,
|
||||
to: "/access-lists",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "ssl.title" }),
|
||||
icon: <Icon as={FiShield} />,
|
||||
subItems: [
|
||||
{
|
||||
label: intl.formatMessage({ id: "certificates.title" }),
|
||||
to: "/ssl/certificates",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "certificate-authorities.title" }),
|
||||
to: "/ssl/authorities",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "dns-providers.title" }),
|
||||
to: "/ssl/dns-providers",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "audit-log.title" }),
|
||||
icon: <Icon as={FiBook} />,
|
||||
to: "/audit-log",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "users.title" }),
|
||||
icon: <Icon as={FiUser} />,
|
||||
to: "/users",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "settings.title" }),
|
||||
icon: <Icon as={FiSettings} />,
|
||||
subItems: [
|
||||
{
|
||||
label: intl.formatMessage({ id: "general-settings.title" }),
|
||||
to: "/settings/general",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "host-templates.title" }),
|
||||
to: "/settings/host-templates",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface NavigationMenuProps {
|
||||
/** Navigation is currently hidden on mobile */
|
||||
mobileNavIsOpen: boolean;
|
||||
closeMobileNav: () => void;
|
||||
}
|
||||
function NavigationMenu({
|
||||
mobileNavIsOpen,
|
||||
closeMobileNav,
|
||||
}: NavigationMenuProps) {
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
return (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<Collapse in={mobileNavIsOpen}>
|
||||
<MobileNavigation closeMobileNav={closeMobileNav} />
|
||||
</Collapse>
|
||||
) : (
|
||||
<DesktopNavigation />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Single tab element for desktop navigation */
|
||||
type NavTabProps = Omit<NavItem, "subItems"> & { active?: boolean };
|
||||
const NavTab = forwardRef<NavTabProps, "a">(
|
||||
({ label, icon, to, active, ...props }, ref) => {
|
||||
const linkColor = useColorModeValue("gray.500", "gray.200");
|
||||
const linkHoverColor = useColorModeValue("gray.900", "white");
|
||||
return (
|
||||
<Link
|
||||
as={RouterLink}
|
||||
ref={ref}
|
||||
height={12}
|
||||
to={to ?? "#"}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
borderBottom="1px solid"
|
||||
borderBottomColor={active ? linkHoverColor : "transparent"}
|
||||
color={active ? linkHoverColor : linkColor}
|
||||
_hover={{
|
||||
textDecoration: "none",
|
||||
color: linkHoverColor,
|
||||
borderBottomColor: linkHoverColor,
|
||||
}}
|
||||
{...props}>
|
||||
{icon}
|
||||
<Text as="span" marginLeft={2}>
|
||||
{label}
|
||||
</Text>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const DesktopNavigation: FC = () => {
|
||||
const path = useLocation().pathname;
|
||||
const activeNavItemIndex = useMemo(
|
||||
() =>
|
||||
navItems.findIndex((item) => {
|
||||
// Find the nav item whose location / sub items location is the beginning of the currently active path
|
||||
if (item.to) {
|
||||
// console.debug(item.to, path);
|
||||
if (item.to === "/") {
|
||||
return path === item.to;
|
||||
}
|
||||
return path.startsWith(item.to !== "" ? item.to : "/dashboard");
|
||||
} else if (item.subItems) {
|
||||
return item.subItems.some((subItem) => path.startsWith(subItem.to));
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
[path],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={{ base: "none", md: "block" }}
|
||||
overflowY="visible"
|
||||
overflowX="auto"
|
||||
whiteSpace="nowrap"
|
||||
borderBottom="1px solid"
|
||||
borderColor={useColorModeValue("gray.200", "gray.700")}>
|
||||
<Container h="full" maxW="container.xl">
|
||||
<HStack spacing={8}>
|
||||
{navItems.map((navItem, index) => {
|
||||
const { subItems, ...propsWithoutSubItems } = navItem;
|
||||
const additionalProps: Partial<NavTabProps> = {};
|
||||
if (index === activeNavItemIndex) {
|
||||
additionalProps["active"] = true;
|
||||
}
|
||||
if (subItems) {
|
||||
return (
|
||||
<Menu key={`mainnav${index}`}>
|
||||
<MenuButton
|
||||
as={NavTab}
|
||||
{...propsWithoutSubItems}
|
||||
{...additionalProps}
|
||||
/>
|
||||
{subItems && (
|
||||
<MenuList>
|
||||
{subItems.map((item, subIndex) => (
|
||||
<MenuItem
|
||||
as={RouterLink}
|
||||
to={item.to}
|
||||
key={`mainnav${index}-${subIndex}`}>
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<NavTab
|
||||
key={`mainnav${index}`}
|
||||
{...propsWithoutSubItems}
|
||||
{...additionalProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</HStack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileNavigation: FC<Pick<NavigationMenuProps, "closeMobileNav">> = ({
|
||||
closeMobileNav,
|
||||
}) => {
|
||||
return (
|
||||
<Stack
|
||||
p={4}
|
||||
display={{ md: "none" }}
|
||||
borderBottom="1px solid"
|
||||
borderColor={useColorModeValue("gray.200", "gray.700")}>
|
||||
{navItems.map((navItem, index) => (
|
||||
<MobileNavItem
|
||||
key={`mainmobilenav${index}`}
|
||||
index={index}
|
||||
closeMobileNav={closeMobileNav}
|
||||
{...navItem}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileNavItem: FC<
|
||||
NavItem & {
|
||||
index: number;
|
||||
closeMobileNav: NavigationMenuProps["closeMobileNav"];
|
||||
}
|
||||
> = ({ closeMobileNav, ...props }) => {
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
|
||||
const onClickHandler = useCallback(() => {
|
||||
if (props.subItems) {
|
||||
// Toggle accordeon
|
||||
onToggle();
|
||||
} else {
|
||||
// Close menu on navigate
|
||||
closeMobileNav();
|
||||
}
|
||||
}, [closeMobileNav, onToggle, props.subItems]);
|
||||
|
||||
return (
|
||||
<Stack spacing={4} onClick={onClickHandler}>
|
||||
<Box>
|
||||
<Flex
|
||||
py={2}
|
||||
as={RouterLink}
|
||||
to={props.to ?? "#"}
|
||||
justify="space-between"
|
||||
align="center"
|
||||
_hover={{
|
||||
textDecoration: "none",
|
||||
}}>
|
||||
<Box display="flex" alignItems="center">
|
||||
{props.icon}
|
||||
<Text as="span" marginLeft={2}>
|
||||
{props.label}
|
||||
</Text>
|
||||
</Box>
|
||||
{props.subItems && (
|
||||
<Icon
|
||||
as={FiChevronDown}
|
||||
transition="all .25s ease-in-out"
|
||||
transform={isOpen ? "rotate(180deg)" : ""}
|
||||
w={6}
|
||||
h={6}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Collapse
|
||||
in={isOpen}
|
||||
animateOpacity
|
||||
style={{ marginTop: "0 !important" }}>
|
||||
<Stack
|
||||
mt={1}
|
||||
pl={4}
|
||||
borderLeft={1}
|
||||
borderStyle="solid"
|
||||
borderColor={useColorModeValue("gray.200", "gray.700")}
|
||||
align="start">
|
||||
{props.subItems &&
|
||||
props.subItems.map((subItem, subIndex) => (
|
||||
<Link
|
||||
as={RouterLink}
|
||||
key={`mainmobilenav${props.index}-${subIndex}`}
|
||||
py={2}
|
||||
onClick={closeMobileNav}
|
||||
to={subItem.to}>
|
||||
{subItem.label}
|
||||
</Link>
|
||||
))}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export { NavigationMenu };
|
Reference in New Issue
Block a user