Skip to content

Commit b8e4e57

Browse files
feat: toggle mobile side menu (#1711)
Adds a toggle in the top bar to show the side menu as a drawer Closes #1415
1 parent 58abb81 commit b8e4e57

8 files changed

Lines changed: 302 additions & 201 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { JSX } from 'react';
2+
3+
import useIntl from 'react-intl/src/components/useIntl';
4+
5+
import {
6+
Drawer,
7+
DrawerContent,
8+
DrawerDescription,
9+
DrawerHeader,
10+
DrawerTitle,
11+
} from '@/components/ui/drawer';
12+
13+
import SideMenuContent from './SideMenuContent';
14+
15+
type MobileSideMenuProps = {
16+
isOpen: boolean;
17+
onClose: () => void;
18+
};
19+
20+
const MobileSideMenu = ({
21+
isOpen,
22+
onClose,
23+
}: MobileSideMenuProps): JSX.Element => {
24+
const { formatMessage } = useIntl();
25+
26+
return (
27+
<Drawer
28+
direction="left"
29+
open={isOpen}
30+
onOpenChange={open => !open && onClose()}
31+
>
32+
{/* Hides the first drawer child which is the drawer handle */}
33+
<DrawerContent className="bg-bg-secondary fixed inset-y-0 right-auto left-0 mt-0 h-full w-auto overflow-y-auto border-none pb-12 [&>div:first-child]:hidden">
34+
<DrawerHeader className="sr-only">
35+
<DrawerTitle>{formatMessage({ id: 'sidemenu.title' })}</DrawerTitle>
36+
<DrawerDescription>
37+
{formatMessage({ id: 'sidemenu.description' })}
38+
</DrawerDescription>
39+
</DrawerHeader>
40+
<SideMenuContent onLinkClick={onClose} />
41+
</DrawerContent>
42+
</Drawer>
43+
);
44+
};
45+
46+
export default MobileSideMenu;

dashboard/src/components/SideMenu/NavLink.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type INavLinkBase = LinkProps & {
1717
asTag?: string;
1818
selected?: boolean;
1919
linkClassName?: string;
20+
onClickElement?: () => void;
2021
};
2122

2223
type INavLinkWithIntl = INavLinkBase & {
@@ -38,6 +39,7 @@ const NavLink = ({
3839
label,
3940
asTag,
4041
linkClassName,
42+
onClickElement,
4143
...props
4244
}: INavLink): JSX.Element => {
4345
const LinkElement = asTag ?? Link;
@@ -58,6 +60,7 @@ const NavLink = ({
5860
selected ? selectedItemClassName : notSelectedItemClassName,
5961
linkClassName,
6062
)}
63+
onClick={onClickElement}
6164
{...props}
6265
>
6366
{icon && <span className="mr-3">{icon}</span>}

dashboard/src/components/SideMenu/SideMenu.tsx

Lines changed: 5 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -1,196 +1,12 @@
1-
import { useMemo, type JSX } from 'react';
1+
import type { JSX } from 'react';
22

3-
import { MdOutlineMonitorHeart } from 'react-icons/md';
4-
5-
import { RxRadiobutton } from 'react-icons/rx';
6-
7-
import { ImTree } from 'react-icons/im';
8-
import { HiOutlineDocumentSearch } from 'react-icons/hi';
9-
10-
import { useLocation } from '@tanstack/react-router';
11-
12-
import { FormattedMessage } from 'react-intl';
13-
14-
import type { MessagesKey } from '@/locales/messages';
15-
16-
import { DOCUMENTATION_URL } from '@/utils/constants/general';
17-
18-
import {
19-
NavigationMenu,
20-
NavigationMenuItem,
21-
NavigationMenuList,
22-
} from '@/components/ui/navigation-menu';
23-
24-
import { Separator } from '@/components/ui/separator';
25-
26-
import type { PossibleMonitorPath } from '@/types/general';
27-
28-
import { ExternalLinkIcon } from '@/components/Icons/ExternalLink';
29-
30-
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/Tooltip';
31-
32-
import SendFeedback from './SendFeedback';
33-
34-
import NavLink from './NavLink';
35-
36-
type RouteMenuItems = {
37-
navigateTo: PossibleMonitorPath;
38-
idIntl: MessagesKey;
39-
icon: JSX.Element;
40-
selected: boolean;
41-
};
42-
43-
type LinkMenuItems = {
44-
url: string;
45-
idIntl: MessagesKey;
46-
icon: JSX.Element;
47-
};
48-
49-
type LinkStringItems = {
50-
url: string;
51-
label: string;
52-
};
53-
54-
const linkItems: LinkMenuItems[] = [
55-
{
56-
url: DOCUMENTATION_URL,
57-
idIntl: 'global.documentation',
58-
icon: <HiOutlineDocumentSearch />,
59-
},
60-
];
61-
62-
const dashboardItems: LinkStringItems[] = [
63-
{
64-
url: 'https://kdevops.org/',
65-
label: 'kdevops',
66-
},
67-
{
68-
url: 'https://netdev.bots.linux.dev/contest.html',
69-
label: 'netdev-CI',
70-
},
71-
];
72-
73-
type SideMenuItemProps = {
74-
item: RouteMenuItems;
75-
};
76-
77-
const SideMenuItem = ({ item }: SideMenuItemProps): JSX.Element => {
78-
const { pathname } = useLocation();
79-
80-
const isCurrentPath =
81-
pathname.startsWith(item.navigateTo) &&
82-
(pathname.length === item.navigateTo.length ||
83-
pathname[item.navigateTo.length] === '/');
84-
85-
return (
86-
<NavigationMenuItem key={item.idIntl} className="w-full">
87-
<NavLink
88-
selected={isCurrentPath}
89-
to={item.navigateTo}
90-
search={prevSearch => ({
91-
origin: prevSearch.origin,
92-
})}
93-
icon={item.icon}
94-
idIntl={item.idIntl}
95-
/>
96-
</NavigationMenuItem>
97-
);
98-
};
3+
import SideMenuContent from './SideMenuContent';
994

1005
const SideMenu = (): JSX.Element => {
101-
const routeItems: RouteMenuItems[] = [
102-
{
103-
navigateTo: '/tree',
104-
idIntl: 'routes.treeMonitor',
105-
icon: <ImTree className="size-5" />,
106-
selected: true,
107-
},
108-
{
109-
navigateTo: '/hardware',
110-
idIntl: 'routes.hardwareMonitor',
111-
icon: <MdOutlineMonitorHeart className="size-5" />,
112-
selected: false,
113-
},
114-
{
115-
navigateTo: '/hardware-new',
116-
idIntl: 'routes.hardwareNewMonitor',
117-
icon: <MdOutlineMonitorHeart className="size-5" />,
118-
selected: false,
119-
},
120-
{
121-
navigateTo: '/issues',
122-
idIntl: 'routes.issueMonitor',
123-
icon: <RxRadiobutton className="size-5" />,
124-
selected: false,
125-
},
126-
];
127-
128-
const linksItemElements = useMemo(
129-
() =>
130-
linkItems.map(item => (
131-
<NavigationMenuItem key={item.idIntl} className="w-full">
132-
<NavLink
133-
asTag="a"
134-
icon={item.icon}
135-
idIntl={item.idIntl}
136-
href={item.url}
137-
target="_blank"
138-
/>
139-
</NavigationMenuItem>
140-
)),
141-
[],
142-
);
143-
144-
const dashboardElements = useMemo(
145-
() =>
146-
dashboardItems.map(item => (
147-
<NavigationMenuItem key={item.label} className="w-full">
148-
<NavLink
149-
asTag="a"
150-
icon={<ExternalLinkIcon />}
151-
label={item.label}
152-
href={item.url}
153-
target="_blank"
154-
/>
155-
</NavigationMenuItem>
156-
)),
157-
[],
158-
);
159-
1606
return (
161-
<NavigationMenu
162-
className="bg-bg-secondary min-h-screen flex-col justify-start pt-6"
163-
orientation="vertical"
164-
>
165-
<div className="w-full px-4">
166-
<img src="/kernelci-logo-white.svg" className="max-w-[125px]" />
167-
</div>
168-
169-
<Separator className="bg-on-secondary-10 my-4" />
170-
171-
<NavigationMenuList className="w-56 flex-col space-y-4 space-x-0">
172-
{routeItems.map(item => (
173-
<SideMenuItem item={item} key={item.idIntl} />
174-
))}
175-
<Separator className="bg-on-secondary-10 my-4" />
176-
{linksItemElements}
177-
<SendFeedback className="w-full" />
178-
<Separator className="bg-on-secondary-10 my-4" />
179-
<Tooltip>
180-
<TooltipTrigger asChild>
181-
<div className="flex items-center gap-2 text-white">
182-
<FormattedMessage id={'sidemenu.communityDashboards'} />
183-
</div>
184-
</TooltipTrigger>
185-
<TooltipContent className="max-w-[450px]">
186-
<FormattedMessage id={'sidemenu.communityDashboardsMsg'} />
187-
</TooltipContent>
188-
</Tooltip>
189-
<div className="flex w-full flex-col space-y-0">
190-
{dashboardElements}
191-
</div>
192-
</NavigationMenuList>
193-
</NavigationMenu>
7+
<div className="bg-bg-secondary hidden md:block">
8+
<SideMenuContent className="min-h-screen" />
9+
</div>
19410
);
19511
};
19612

0 commit comments

Comments
 (0)