diff --git a/dashboard/src/components/SideMenu/MobileSideMenu.tsx b/dashboard/src/components/SideMenu/MobileSideMenu.tsx new file mode 100644 index 000000000..3e912860f --- /dev/null +++ b/dashboard/src/components/SideMenu/MobileSideMenu.tsx @@ -0,0 +1,46 @@ +import type { JSX } from 'react'; + +import useIntl from 'react-intl/src/components/useIntl'; + +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, +} from '@/components/ui/drawer'; + +import SideMenuContent from './SideMenuContent'; + +type MobileSideMenuProps = { + isOpen: boolean; + onClose: () => void; +}; + +const MobileSideMenu = ({ + isOpen, + onClose, +}: MobileSideMenuProps): JSX.Element => { + const { formatMessage } = useIntl(); + + return ( + !open && onClose()} + > + {/* Hides the first drawer child which is the drawer handle */} + + + {formatMessage({ id: 'sidemenu.title' })} + + {formatMessage({ id: 'sidemenu.description' })} + + + + + + ); +}; + +export default MobileSideMenu; diff --git a/dashboard/src/components/SideMenu/NavLink.tsx b/dashboard/src/components/SideMenu/NavLink.tsx index f47a042df..899cc7eb3 100644 --- a/dashboard/src/components/SideMenu/NavLink.tsx +++ b/dashboard/src/components/SideMenu/NavLink.tsx @@ -17,6 +17,7 @@ type INavLinkBase = LinkProps & { asTag?: string; selected?: boolean; linkClassName?: string; + onClickElement?: () => void; }; type INavLinkWithIntl = INavLinkBase & { @@ -38,6 +39,7 @@ const NavLink = ({ label, asTag, linkClassName, + onClickElement, ...props }: INavLink): JSX.Element => { const LinkElement = asTag ?? Link; @@ -58,6 +60,7 @@ const NavLink = ({ selected ? selectedItemClassName : notSelectedItemClassName, linkClassName, )} + onClick={onClickElement} {...props} > {icon && {icon}} diff --git a/dashboard/src/components/SideMenu/SideMenu.tsx b/dashboard/src/components/SideMenu/SideMenu.tsx index 28975be4d..75e693159 100644 --- a/dashboard/src/components/SideMenu/SideMenu.tsx +++ b/dashboard/src/components/SideMenu/SideMenu.tsx @@ -1,196 +1,12 @@ -import { useMemo, type JSX } from 'react'; +import type { JSX } from 'react'; -import { MdOutlineMonitorHeart } from 'react-icons/md'; - -import { RxRadiobutton } from 'react-icons/rx'; - -import { ImTree } from 'react-icons/im'; -import { HiOutlineDocumentSearch } from 'react-icons/hi'; - -import { useLocation } from '@tanstack/react-router'; - -import { FormattedMessage } from 'react-intl'; - -import type { MessagesKey } from '@/locales/messages'; - -import { DOCUMENTATION_URL } from '@/utils/constants/general'; - -import { - NavigationMenu, - NavigationMenuItem, - NavigationMenuList, -} from '@/components/ui/navigation-menu'; - -import { Separator } from '@/components/ui/separator'; - -import type { PossibleMonitorPath } from '@/types/general'; - -import { ExternalLinkIcon } from '@/components/Icons/ExternalLink'; - -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/Tooltip'; - -import SendFeedback from './SendFeedback'; - -import NavLink from './NavLink'; - -type RouteMenuItems = { - navigateTo: PossibleMonitorPath; - idIntl: MessagesKey; - icon: JSX.Element; - selected: boolean; -}; - -type LinkMenuItems = { - url: string; - idIntl: MessagesKey; - icon: JSX.Element; -}; - -type LinkStringItems = { - url: string; - label: string; -}; - -const linkItems: LinkMenuItems[] = [ - { - url: DOCUMENTATION_URL, - idIntl: 'global.documentation', - icon: , - }, -]; - -const dashboardItems: LinkStringItems[] = [ - { - url: 'https://kdevops.org/', - label: 'kdevops', - }, - { - url: 'https://netdev.bots.linux.dev/contest.html', - label: 'netdev-CI', - }, -]; - -type SideMenuItemProps = { - item: RouteMenuItems; -}; - -const SideMenuItem = ({ item }: SideMenuItemProps): JSX.Element => { - const { pathname } = useLocation(); - - const isCurrentPath = - pathname.startsWith(item.navigateTo) && - (pathname.length === item.navigateTo.length || - pathname[item.navigateTo.length] === '/'); - - return ( - - ({ - origin: prevSearch.origin, - })} - icon={item.icon} - idIntl={item.idIntl} - /> - - ); -}; +import SideMenuContent from './SideMenuContent'; const SideMenu = (): JSX.Element => { - const routeItems: RouteMenuItems[] = [ - { - navigateTo: '/tree', - idIntl: 'routes.treeMonitor', - icon: , - selected: true, - }, - { - navigateTo: '/hardware', - idIntl: 'routes.hardwareMonitor', - icon: , - selected: false, - }, - { - navigateTo: '/hardware-new', - idIntl: 'routes.hardwareNewMonitor', - icon: , - selected: false, - }, - { - navigateTo: '/issues', - idIntl: 'routes.issueMonitor', - icon: , - selected: false, - }, - ]; - - const linksItemElements = useMemo( - () => - linkItems.map(item => ( - - - - )), - [], - ); - - const dashboardElements = useMemo( - () => - dashboardItems.map(item => ( - - } - label={item.label} - href={item.url} - target="_blank" - /> - - )), - [], - ); - return ( - -
- -
- - - - - {routeItems.map(item => ( - - ))} - - {linksItemElements} - - - - -
- -
-
- - - -
-
- {dashboardElements} -
-
-
+
+ +
); }; diff --git a/dashboard/src/components/SideMenu/SideMenuContent.tsx b/dashboard/src/components/SideMenu/SideMenuContent.tsx new file mode 100644 index 000000000..31fde4cf7 --- /dev/null +++ b/dashboard/src/components/SideMenu/SideMenuContent.tsx @@ -0,0 +1,132 @@ +import { useMemo, type JSX } from 'react'; + +import { useLocation } from '@tanstack/react-router'; + +import { FormattedMessage } from 'react-intl'; + +import { + NavigationMenu, + NavigationMenuItem, + NavigationMenuList, +} from '@/components/ui/navigation-menu'; + +import { Separator } from '@/components/ui/separator'; + +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/Tooltip'; + +import { ExternalLinkIcon } from '@/components/Icons/ExternalLink'; + +import SendFeedback from './SendFeedback'; +import NavLink from './NavLink'; +import { + routeItems, + linkItems, + dashboardItems, + type RouteMenuItems, +} from './menuItems'; + +type SideMenuItemProps = { + item: RouteMenuItems; +}; + +const SideMenuItem = ({ item }: SideMenuItemProps): JSX.Element => { + const { pathname } = useLocation(); + + const isCurrentPath = + pathname.startsWith(item.navigateTo) && + (pathname.length === item.navigateTo.length || + pathname[item.navigateTo.length] === '/'); + + return ( + + ({ + origin: prevSearch.origin, + })} + icon={item.icon} + idIntl={item.idIntl} + /> + + ); +}; + +type SideMenuContentProps = { + onLinkClick?: () => void; + className?: string; +}; + +const SideMenuContent = ({ + onLinkClick, + className = '', +}: SideMenuContentProps): JSX.Element => { + const linksItemElements = useMemo( + () => + linkItems.map(item => ( + + + + )), + [onLinkClick], + ); + + const dashboardElements = useMemo( + () => + dashboardItems.map(item => ( + + } + label={item.label} + href={item.url} + target="_blank" + onClickElement={onLinkClick} + /> + + )), + [onLinkClick], + ); + + return ( + +
+ +
+ + + {routeItems.map(item => ( + + ))} + + {linksItemElements} + + + + +
+ +
+
+ + + +
+
+ {dashboardElements} +
+
+
+ ); +}; + +export default SideMenuContent; diff --git a/dashboard/src/components/SideMenu/menuItems.tsx b/dashboard/src/components/SideMenu/menuItems.tsx new file mode 100644 index 000000000..9f693f01a --- /dev/null +++ b/dashboard/src/components/SideMenu/menuItems.tsx @@ -0,0 +1,79 @@ +import type { JSX } from 'react'; + +import { MdOutlineMonitorHeart } from 'react-icons/md'; +import { RxRadiobutton } from 'react-icons/rx'; +import { ImTree } from 'react-icons/im'; +import { HiOutlineDocumentSearch } from 'react-icons/hi'; + +import type { MessagesKey } from '@/locales/messages'; +import { DOCUMENTATION_URL } from '@/utils/constants/general'; +import type { PossibleMonitorPath } from '@/types/general'; + +export type RouteMenuItems = { + navigateTo: PossibleMonitorPath; + idIntl: MessagesKey; + icon: JSX.Element; + selected: boolean; +}; + +export type LinkMenuItems = { + url: string; + idIntl: MessagesKey; + icon: JSX.Element; +}; + +export type LinkStringItems = { + url: string; + label: string; +}; + +const TreeIcon = ; +const MonitorHeartIcon = ; +const RadioButtonIcon = ; +const DocumentSearchIcon = ; + +export const routeItems: RouteMenuItems[] = [ + { + navigateTo: '/tree', + idIntl: 'routes.treeMonitor', + icon: TreeIcon, + selected: true, + }, + { + navigateTo: '/hardware', + idIntl: 'routes.hardwareMonitor', + icon: MonitorHeartIcon, + selected: false, + }, + { + navigateTo: '/hardware-new', + idIntl: 'routes.hardwareNewMonitor', + icon: MonitorHeartIcon, + selected: false, + }, + { + navigateTo: '/issues', + idIntl: 'routes.issueMonitor', + icon: RadioButtonIcon, + selected: false, + }, +]; + +export const linkItems: LinkMenuItems[] = [ + { + url: DOCUMENTATION_URL, + idIntl: 'global.documentation', + icon: DocumentSearchIcon, + }, +]; + +export const dashboardItems: LinkStringItems[] = [ + { + url: 'https://kdevops.org/', + label: 'kdevops', + }, + { + url: 'https://netdev.bots.linux.dev/contest.html', + label: 'netdev-CI', + }, +]; diff --git a/dashboard/src/components/TopBar/TopBar.tsx b/dashboard/src/components/TopBar/TopBar.tsx index 10a3f099d..c9bec8650 100644 --- a/dashboard/src/components/TopBar/TopBar.tsx +++ b/dashboard/src/components/TopBar/TopBar.tsx @@ -7,11 +7,15 @@ import { useMatches, } from '@tanstack/react-router'; -import { useCallback, useEffect, useMemo, type JSX } from 'react'; +import { useCallback, useEffect, useMemo, useState, type JSX } from 'react'; + +import { HiMenu } from 'react-icons/hi'; import Select, { SelectItem } from '@/components/Select/Select'; import { DEFAULT_ORIGIN, type PossibleMonitorPath } from '@/types/general'; import { useOrigins } from '@/api/origin'; +import { Button } from '@/components/ui/button'; +import MobileSideMenu from '@/components/SideMenu/MobileSideMenu'; const getTargetPath = (basePath: string): PossibleMonitorPath => { switch (basePath) { @@ -119,6 +123,7 @@ const TitleName = ({ basePath }: { basePath: string }): JSX.Element => { }; const TopBar = (): JSX.Element => { + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const matches = useMatches(); const redirectStateFrom = useRouterState({ select: s => s.location.state.from, @@ -139,16 +144,33 @@ const TopBar = (): JSX.Element => { const basePath = redirectStateFrom ?? routeInfo.firstUrlLocation; return ( -
-
- - - - {(routeInfo.isTreeListing || routeInfo.isHardwarePage) && ( - - )} + <> +
+
+
+ + + + + {(routeInfo.isTreeListing || routeInfo.isHardwarePage) && ( + + )} +
+
-
+ setIsMobileMenuOpen(false)} + /> + ); }; diff --git a/dashboard/src/locales/messages/index.ts b/dashboard/src/locales/messages/index.ts index d40915e7b..cfda40953 100644 --- a/dashboard/src/locales/messages/index.ts +++ b/dashboard/src/locales/messages/index.ts @@ -269,9 +269,12 @@ export const messages = { 'sidemenu.communityDashboards': 'Community Dashboards', 'sidemenu.communityDashboardsMsg': 'Across the Linux kernel community, there are other dashboards available that you might find useful for exploring different kernel testing data and perspectives.', + 'sidemenu.description': + 'Links to different sections of the dashboard, useful links and external resources.', 'sidemenu.sendFeedback': 'Send us Feedback', 'sidemenu.sendFeedbackMsg': 'Thank you for your feedback!\nWe greatly appreciate your input. You are welcome to send us the feedback via email or by creating an issue on our GitHub repository.', + 'sidemenu.title': 'Navigation Menu', 'tab.findOnPreviousCheckoutsTooltip': 'You may still find {tab} on previous checkouts', 'tab.name': 'Name', diff --git a/dashboard/src/routes/_main/route.tsx b/dashboard/src/routes/_main/route.tsx index e793d6811..cbb71dd46 100644 --- a/dashboard/src/routes/_main/route.tsx +++ b/dashboard/src/routes/_main/route.tsx @@ -20,9 +20,9 @@ const RouteComponent = (): JSX.Element => {
-
+
-
+