mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-10-10 06:22:09 +00:00
Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager
This commit is contained in:
24
frontend/src/components/Navigation/Navigation.tsx
Normal file
24
frontend/src/components/Navigation/Navigation.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { NavigationHeader, NavigationMenu } from "components";
|
||||
|
||||
function Navigation() {
|
||||
const {
|
||||
isOpen: mobileNavIsOpen,
|
||||
onToggle: mobileNavToggle,
|
||||
onClose: mobileNavClose,
|
||||
} = useDisclosure();
|
||||
return (
|
||||
<>
|
||||
<NavigationHeader
|
||||
toggleMobileNav={mobileNavToggle}
|
||||
mobileNavIsOpen={mobileNavIsOpen}
|
||||
/>
|
||||
<NavigationMenu
|
||||
mobileNavIsOpen={mobileNavIsOpen}
|
||||
closeMobileNav={mobileNavClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { Navigation };
|
113
frontend/src/components/Navigation/NavigationHeader.tsx
Normal file
113
frontend/src/components/Navigation/NavigationHeader.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
chakra,
|
||||
Container,
|
||||
Flex,
|
||||
HStack,
|
||||
Icon,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { ThemeSwitcher } from "components";
|
||||
import { useAuthState } from "context";
|
||||
import { useUser } from "hooks";
|
||||
import { intl } from "locale";
|
||||
import { ChangePasswordModal, ProfileModal } from "modals";
|
||||
import { FiLock, FiLogOut, FiMenu, FiUser, FiX } from "react-icons/fi";
|
||||
|
||||
interface NavigationHeaderProps {
|
||||
mobileNavIsOpen: boolean;
|
||||
toggleMobileNav: () => void;
|
||||
}
|
||||
function NavigationHeader({
|
||||
mobileNavIsOpen,
|
||||
toggleMobileNav,
|
||||
}: NavigationHeaderProps) {
|
||||
const passwordDisclosure = useDisclosure();
|
||||
const profileDisclosure = useDisclosure();
|
||||
const { data: user } = useUser("me");
|
||||
const { logout } = useAuthState();
|
||||
|
||||
return (
|
||||
<Box
|
||||
h={16}
|
||||
borderBottom="1px solid"
|
||||
borderColor={useColorModeValue("gray.200", "gray.700")}>
|
||||
<Container h="full" maxW="container.xl">
|
||||
<Flex h="full" alignItems="center" justifyContent="space-between">
|
||||
<IconButton
|
||||
display={{ base: "block", md: "none" }}
|
||||
position="relative"
|
||||
bg="transparent"
|
||||
aria-label={
|
||||
mobileNavIsOpen
|
||||
? intl.formatMessage({ id: "navigation.close" })
|
||||
: intl.formatMessage({ id: "navigation.open" })
|
||||
}
|
||||
onClick={toggleMobileNav}
|
||||
icon={<Icon as={mobileNavIsOpen ? FiX : FiMenu} />}
|
||||
/>
|
||||
<HStack height="full" paddingY={3} spacing={4}>
|
||||
<chakra.img src="/images/logo-no-text.svg" alt="" height="full" />
|
||||
<Text
|
||||
display={{ base: "none", md: "block" }}
|
||||
fontSize="2xl"
|
||||
fontWeight="bold">
|
||||
{intl.formatMessage({ id: "brand.name" })}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack>
|
||||
<ThemeSwitcher background="transparent" />
|
||||
<Box pl={2}>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rounded="full"
|
||||
variant="link"
|
||||
cursor="pointer"
|
||||
minW={0}>
|
||||
<Avatar size="sm" src={user?.gravatarUrl} />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
icon={<Icon as={FiUser} />}
|
||||
onClick={profileDisclosure.onOpen}>
|
||||
{intl.formatMessage({ id: "profile.title" })}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<Icon as={FiLock} />}
|
||||
onClick={passwordDisclosure.onOpen}>
|
||||
{intl.formatMessage({ id: "change-password" })}
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem onClick={logout} icon={<Icon as={FiLogOut} />}>
|
||||
{intl.formatMessage({ id: "profile.logout" })}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Container>
|
||||
<ProfileModal
|
||||
isOpen={profileDisclosure.isOpen}
|
||||
onClose={profileDisclosure.onClose}
|
||||
/>
|
||||
<ChangePasswordModal
|
||||
isOpen={passwordDisclosure.isOpen}
|
||||
onClose={passwordDisclosure.onClose}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export { NavigationHeader };
|
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 };
|
3
frontend/src/components/Navigation/index.ts
Normal file
3
frontend/src/components/Navigation/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./Navigation";
|
||||
export * from "./NavigationHeader";
|
||||
export * from "./NavigationMenu";
|
Reference in New Issue
Block a user