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) && (
-
- )}
+ <>
+
+
+
+ setIsMobileMenuOpen(true)}
+ aria-label="Open menu"
+ >
+
+
+
+
+
+ {(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 => {