Skip to content

Commit b20150f

Browse files
Making top nav menu responsive (#16618)
1 parent 0a67f38 commit b20150f

File tree

5 files changed

+186
-102
lines changed

5 files changed

+186
-102
lines changed

components/dashboard/src/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { useEffect } from "react";
88
import { useLocation } from "react-router";
9-
import Separator from "./Separator";
9+
import { Separator } from "./Separator";
1010
import TabMenuItem from "./TabMenuItem";
1111

1212
export interface HeaderProps {

components/dashboard/src/components/Separator.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
export default function Separator() {
8-
return <div className="border-gray-200 dark:border-gray-800 border-b absolute left-0 w-full"></div>;
9-
}
7+
import classNames from "classnames";
8+
import { FC } from "react";
9+
10+
type Props = {
11+
className?: string;
12+
};
13+
export const Separator: FC<Props> = ({ className }) => {
14+
return (
15+
<div
16+
className={classNames("border-gray-200 dark:border-gray-800 border-b absolute left-0 w-full", className)}
17+
/>
18+
);
19+
};

components/dashboard/src/index.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
}
3636

3737
.app-container {
38-
@apply lg:px-28 px-10;
38+
@apply lg:px-28 px-4;
3939
}
4040
.btn-login {
4141
@apply rounded-md border-none bg-gray-100 hover:bg-gray-200 text-gray-500 dark:text-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600;

components/dashboard/src/menu/Menu.tsx

Lines changed: 170 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,23 @@
55
*/
66

77
import { User } from "@gitpod/gitpod-protocol";
8-
import { useContext, useEffect, useState } from "react";
8+
import { FC, useCallback, useContext, useEffect, useMemo, useState } from "react";
99
import { Link } from "react-router-dom";
1010
import { useLocation } from "react-router";
1111
import { Location } from "history";
1212
import { countries } from "countries-list";
1313
import gitpodIcon from "../icons/gitpod.svg";
1414
import { getGitpodService, gitpodHostUrl } from "../service/service";
1515
import { useCurrentUser } from "../user-context";
16-
import ContextMenu from "../components/ContextMenu";
17-
import Separator from "../components/Separator";
16+
import ContextMenu, { ContextMenuEntry } from "../components/ContextMenu";
17+
import { Separator } from "../components/Separator";
1818
import PillMenuItem from "../components/PillMenuItem";
1919
import { PaymentContext } from "../payment-context";
2020
import FeedbackFormModal from "../feedback-form/FeedbackModal";
2121
import { isGitpodIo } from "../utils";
2222
import OrganizationSelector from "./OrganizationSelector";
2323
import { getAdminTabs } from "../admin/admin.routes";
24+
import classNames from "classnames";
2425

2526
interface Entry {
2627
title: string;
@@ -34,12 +35,6 @@ export default function Menu() {
3435
const { setCurrency, setIsStudent, setIsChargebeeCustomer } = useContext(PaymentContext);
3536
const [isFeedbackFormVisible, setFeedbackFormVisible] = useState<boolean>(false);
3637

37-
function isSelected(entry: Entry, location: Location<any>) {
38-
const all = [entry.link, ...(entry.alternatives || [])].map((l) => l.toLowerCase());
39-
const path = location.pathname.toLowerCase();
40-
return all.some((n) => n === path || n + "/" === path);
41-
}
42-
4338
useEffect(() => {
4439
const { server } = getGitpodService();
4540
Promise.all([
@@ -52,64 +47,49 @@ export default function Menu() {
5247
]).then((setters) => setters.forEach((s) => s()));
5348
}, [setCurrency, setIsChargebeeCustomer, setIsStudent]);
5449

55-
const leftMenu: Entry[] = [
56-
{
57-
title: "Workspaces",
58-
link: "/workspaces",
59-
alternatives: ["/"],
60-
},
61-
{
62-
title: "Projects",
63-
link: `/projects`,
64-
alternatives: [] as string[],
65-
},
66-
];
67-
68-
const adminMenu: Entry = {
69-
title: "Admin",
70-
link: "/admin",
71-
alternatives: [
72-
...getAdminTabs().reduce(
73-
(prevEntry, currEntry) =>
74-
currEntry.alternatives
75-
? [...prevEntry, ...currEntry.alternatives, currEntry.link]
76-
: [...prevEntry, currEntry.link],
77-
[] as string[],
78-
),
79-
],
80-
};
50+
const adminMenu: Entry = useMemo(
51+
() => ({
52+
title: "Admin",
53+
link: "/admin",
54+
alternatives: [
55+
...getAdminTabs().reduce(
56+
(prevEntry, currEntry) =>
57+
currEntry.alternatives
58+
? [...prevEntry, ...currEntry.alternatives, currEntry.link]
59+
: [...prevEntry, currEntry.link],
60+
[] as string[],
61+
),
62+
],
63+
}),
64+
[],
65+
);
8166

82-
const handleFeedbackFormClick = () => {
67+
const handleFeedbackFormClick = useCallback(() => {
8368
setFeedbackFormVisible(true);
84-
};
69+
}, []);
8570

86-
const onFeedbackFormClose = () => {
71+
const onFeedbackFormClose = useCallback(() => {
8772
setFeedbackFormVisible(false);
88-
};
73+
}, []);
8974

9075
return (
9176
<>
92-
<header className="app-container flex flex-col pt-4 space-y-4" data-analytics='{"button_type":"menu"}'>
93-
<div className="flex h-10 mb-3">
94-
<div className="flex justify-between items-center pr-3">
95-
<Link to="/" className="pr-3 w-10">
77+
<header className="app-container flex flex-col pt-4" data-analytics='{"button_type":"menu"}'>
78+
<div className="flex justify-between h-10 mb-3 w-full">
79+
<div className="flex items-center">
80+
{/* hidden on smaller screens */}
81+
<Link to="/" className="hidden md:inline pr-3 w-10">
9682
<img src={gitpodIcon} className="h-6" alt="Gitpod's logo" />
9783
</Link>
9884
<OrganizationSelector />
99-
<div className="pl-2 text-base text-gray-500 dark:text-gray-400 flex max-w-lg overflow-hidden">
100-
{leftMenu.map((entry) => (
101-
<div className="p-1" key={entry.title}>
102-
<PillMenuItem
103-
name={entry.title}
104-
selected={isSelected(entry, location)}
105-
link={entry.link}
106-
/>
107-
</div>
108-
))}
85+
{/* hidden on smaller screens (in it's own menu below on smaller screens) */}
86+
<div className="hidden md:block pl-2">
87+
<OrgPagesNav />
10988
</div>
11089
</div>
111-
<div className="flex-1 flex items-center w-auto" id="menu">
112-
<nav className="flex-1">
90+
<div className="flex items-center w-auto" id="menu">
91+
{/* hidden on smaller screens - TODO: move to user menu on smaller screen */}
92+
<nav className="hidden md:block flex-1">
11393
<ul className="flex flex-1 items-center justify-between text-base text-gray-500 dark:text-gray-400 space-x-2">
11494
<li className="flex-1"></li>
11595
{user?.rolesOrPermissions?.includes("admin") && (
@@ -128,52 +108,146 @@ export default function Menu() {
128108
)}
129109
</ul>
130110
</nav>
131-
<div
132-
className="ml-3 flex items-center justify-start mb-0 pointer-cursor m-l-auto rounded-full border-2 border-transparent hover:border-gray-200 dark:hover:border-gray-700 p-0.5 font-medium flex-shrink-0"
133-
data-analytics='{"label":"Account"}'
134-
>
135-
<ContextMenu
136-
menuEntries={[
137-
{
138-
title: (user && (User.getPrimaryEmail(user) || user?.name)) || "User",
139-
customFontStyle: "text-gray-400",
140-
separator: true,
141-
},
142-
{
143-
title: "User Settings",
144-
link: "/user/settings",
145-
},
146-
{
147-
title: "Docs",
148-
href: "https://www.gitpod.io/docs/",
149-
target: "_blank",
150-
rel: "noreferrer",
151-
},
152-
{
153-
title: "Help",
154-
href: "https://www.gitpod.io/support/",
155-
target: "_blank",
156-
rel: "noreferrer",
157-
separator: true,
158-
},
159-
{
160-
title: "Logout",
161-
href: gitpodHostUrl.asApiLogout().toString(),
162-
},
163-
]}
164-
>
165-
<img
166-
className="rounded-full w-6 h-6"
167-
src={user?.avatarUrl || ""}
168-
alt={user?.name || "Anonymous"}
169-
/>
170-
</ContextMenu>
171-
</div>
111+
{/* Hide normal user menu on small screens */}
112+
<UserMenu user={user} className="hidden md:block" />
113+
{/* Show a user menu w/ admin & feedback links on small screens */}
114+
<UserMenu
115+
user={user}
116+
className="md:hidden"
117+
withAdminLink
118+
withFeedbackLink
119+
onFeedback={handleFeedbackFormClick}
120+
/>
172121
</div>
173122
{isFeedbackFormVisible && <FeedbackFormModal onClose={onFeedbackFormClose} />}
174123
</div>
175124
</header>
176125
<Separator />
126+
{/* only shown on small screens */}
127+
<OrgPagesNav className="md:hidden app-container flex justify-start py-2" />
128+
{/* only shown on small screens */}
129+
<Separator className="md:hidden" />
177130
</>
178131
);
179132
}
133+
134+
type OrgPagesNavProps = {
135+
className?: string;
136+
};
137+
const OrgPagesNav: FC<OrgPagesNavProps> = ({ className }) => {
138+
const location = useLocation();
139+
140+
const leftMenu: Entry[] = useMemo(
141+
() => [
142+
{
143+
title: "Workspaces",
144+
link: "/workspaces",
145+
alternatives: ["/"],
146+
},
147+
{
148+
title: "Projects",
149+
link: `/projects`,
150+
alternatives: [] as string[],
151+
},
152+
],
153+
[],
154+
);
155+
156+
return (
157+
<div
158+
className={classNames(
159+
"text-base text-gray-500 dark:text-gray-400 flex items-center space-x-1 py-1",
160+
className,
161+
)}
162+
>
163+
{leftMenu.map((entry) => (
164+
<div key={entry.title}>
165+
<PillMenuItem name={entry.title} selected={isSelected(entry, location)} link={entry.link} />
166+
</div>
167+
))}
168+
</div>
169+
);
170+
};
171+
172+
type UserMenuProps = {
173+
user?: User;
174+
className?: string;
175+
withAdminLink?: boolean;
176+
withFeedbackLink?: boolean;
177+
onFeedback?: () => void;
178+
};
179+
const UserMenu: FC<UserMenuProps> = ({ user, className, withAdminLink, withFeedbackLink, onFeedback }) => {
180+
const extraSection = useMemo(() => {
181+
const items: ContextMenuEntry[] = [];
182+
183+
if (withAdminLink && user?.rolesOrPermissions?.includes("admin")) {
184+
items.push({
185+
title: "Admin",
186+
link: "/admin",
187+
});
188+
}
189+
if (withFeedbackLink && isGitpodIo()) {
190+
items.push({
191+
title: "Feedback",
192+
onClick: onFeedback,
193+
});
194+
}
195+
196+
// Add a separator to the last item
197+
if (items.length > 0) {
198+
items[items.length - 1].separator = true;
199+
}
200+
201+
return items;
202+
}, [onFeedback, user?.rolesOrPermissions, withAdminLink, withFeedbackLink]);
203+
204+
return (
205+
<div
206+
className={classNames(
207+
"ml-3 flex items-center justify-start mb-0 pointer-cursor m-l-auto rounded-full border-2 border-transparent hover:border-gray-200 dark:hover:border-gray-700 p-0.5 font-medium flex-shrink-0",
208+
className,
209+
)}
210+
data-analytics='{"label":"Account"}'
211+
>
212+
<ContextMenu
213+
menuEntries={[
214+
{
215+
title: (user && (User.getPrimaryEmail(user) || user?.name)) || "User",
216+
customFontStyle: "text-gray-400",
217+
separator: true,
218+
},
219+
{
220+
title: "User Settings",
221+
link: "/user/settings",
222+
},
223+
{
224+
title: "Docs",
225+
href: "https://www.gitpod.io/docs/",
226+
target: "_blank",
227+
rel: "noreferrer",
228+
},
229+
{
230+
title: "Help",
231+
href: "https://www.gitpod.io/support/",
232+
target: "_blank",
233+
rel: "noreferrer",
234+
separator: true,
235+
},
236+
...extraSection,
237+
{
238+
title: "Logout",
239+
href: gitpodHostUrl.asApiLogout().toString(),
240+
},
241+
]}
242+
>
243+
<img className="rounded-full w-8 h-8" src={user?.avatarUrl || ""} alt={user?.name || "Anonymous"} />
244+
</ContextMenu>
245+
</div>
246+
);
247+
};
248+
249+
function isSelected(entry: Entry, location: Location<any>) {
250+
const all = [entry.link, ...(entry.alternatives || [])].map((l) => l.toLowerCase());
251+
const path = location.pathname.toLowerCase();
252+
return all.some((n) => n === path || n + "/" === path);
253+
}

components/dashboard/src/onboarding/UserOnboarding.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { User } from "@gitpod/gitpod-protocol";
88
import { FunctionComponent, useCallback, useContext, useState } from "react";
99
import gitpodIcon from "../icons/gitpod.svg";
10-
import Separator from "../components/Separator";
10+
import { Separator } from "../components/Separator";
1111
import { useHistory, useLocation } from "react-router";
1212
import { StepUserInfo } from "./StepUserInfo";
1313
import { UserContext } from "../user-context";

0 commit comments

Comments
 (0)