Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/menu/Menu.tsx
2500 views
1
/**
2
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import { FC, useCallback, useContext, useEffect, useMemo, useState } from "react";
8
import { useLocation } from "react-router";
9
import { Location } from "history";
10
import { countries } from "countries-list";
11
import { getGitpodService, gitpodHostUrl } from "../service/service";
12
import { useCurrentUser } from "../user-context";
13
import ContextMenu, { ContextMenuEntry } from "../components/ContextMenu";
14
import { Separator } from "../components/Separator";
15
import PillMenuItem from "../components/PillMenuItem";
16
import { PaymentContext } from "../payment-context";
17
import FeedbackFormModal from "../feedback-form/FeedbackModal";
18
import OrganizationSelector from "./OrganizationSelector";
19
import { getAdminTabs } from "../admin/admin.routes";
20
import classNames from "classnames";
21
import { User, RoleOrPermission } from "@gitpod/public-api/lib/gitpod/v1/user_pb";
22
import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils";
23
import { ConfigurationsMigrationCoachmark } from "../repositories/coachmarks/MigrationCoachmark";
24
import { useInstallationConfiguration } from "../data/installation/installation-config-query";
25
import { useIsDataOps } from "../data/featureflag-query";
26
import { ProductLogo } from "../components/ProductLogo";
27
28
interface Entry {
29
title: string;
30
link: string;
31
alternatives?: string[];
32
}
33
34
export default function Menu() {
35
const user = useCurrentUser();
36
const location = useLocation();
37
const { setCurrency } = useContext(PaymentContext);
38
const [isFeedbackFormVisible, setFeedbackFormVisible] = useState<boolean>(false);
39
const isDataOps = useIsDataOps();
40
41
useEffect(() => {
42
const { server } = getGitpodService();
43
server.getClientRegion().then((v) => {
44
// @ts-ignore
45
setCurrency(countries[v]?.currency === "EUR" ? "EUR" : "USD");
46
});
47
}, [setCurrency]);
48
49
const adminMenu: Entry = useMemo(
50
() => ({
51
title: "Admin",
52
link: "/admin",
53
alternatives: [
54
...getAdminTabs().reduce(
55
(prevEntry, currEntry) =>
56
currEntry.alternatives
57
? [...prevEntry, ...currEntry.alternatives, currEntry.link]
58
: [...prevEntry, currEntry.link],
59
[] as string[],
60
),
61
],
62
}),
63
[],
64
);
65
66
const handleFeedbackFormClick = useCallback(() => {
67
setFeedbackFormVisible(true);
68
}, []);
69
70
const onFeedbackFormClose = useCallback(() => {
71
setFeedbackFormVisible(false);
72
}, []);
73
74
return (
75
<>
76
<header className="app-container flex flex-col pt-4" data-analytics='{"button_type":"menu"}'>
77
<div className="flex justify-between h-10 mb-3 w-full">
78
<div className="flex items-center">
79
<ConfigurationsMigrationCoachmark>
80
<OrganizationSelector />
81
</ConfigurationsMigrationCoachmark>
82
{/* Mobile Only Divider and User Menu */}
83
<div className="flex items-center md:hidden">
84
<div className="h-6 w-px bg-gray-300 dark:bg-gray-600 mx-2" />
85
<UserMenu user={user} className="" onFeedback={handleFeedbackFormClick} withAdminLink />
86
</div>
87
{/* Desktop Only Divider, User Menu, and Workspaces Nav */}
88
<div className="hidden md:flex items-center">
89
<div className="h-6 w-px bg-gray-300 dark:bg-gray-600 mx-2" />
90
<UserMenu user={user} className="" onFeedback={handleFeedbackFormClick} />
91
<div className="pl-4">
92
<OrgPagesNav />
93
</div>
94
</div>
95
</div>
96
<div className="flex items-center w-auto" id="menu">
97
{/* Right side nav - Desktop Only */}
98
<nav className="hidden md:block flex-1">
99
<ul className="flex flex-1 items-center justify-end text-base text-gray-500 dark:text-gray-400 space-x-4">
100
{user?.rolesOrPermissions?.includes(RoleOrPermission.ADMIN) && (
101
<li className="cursor-pointer">
102
<PillMenuItem
103
name="Admin"
104
selected={isSelected(adminMenu, location)}
105
link="/admin"
106
/>
107
</li>
108
)}
109
{!isDataOps && (
110
<li>
111
<div className="flex items-center gap-x-1 text-sm text-pk-content-secondary">
112
<ProductLogo className="h-4 w-auto" />
113
<span>Gitpod Classic</span>
114
</div>
115
</li>
116
)}
117
</ul>
118
</nav>
119
{/* Right side items - Mobile Only */}
120
<div className="flex items-center space-x-3 md:hidden">
121
{!isDataOps && (
122
<div className="flex items-center gap-x-1 text-sm text-pk-content-secondary">
123
<ProductLogo className="h-4 w-auto" />
124
<span>Gitpod Classic</span>
125
</div>
126
)}
127
</div>
128
</div>
129
{isFeedbackFormVisible && <FeedbackFormModal onClose={onFeedbackFormClose} />}
130
</div>
131
</header>
132
<Separator />
133
{/* Mobile-only OrgPagesNav and Separator */}
134
<OrgPagesNav className="md:hidden app-container flex justify-start py-2" />
135
<Separator className="md:hidden" />
136
</>
137
);
138
}
139
140
const leftMenu: Entry[] = [
141
{
142
title: "Workspaces",
143
link: "/workspaces",
144
alternatives: ["/"],
145
},
146
];
147
148
type OrgPagesNavProps = {
149
className?: string;
150
};
151
const OrgPagesNav: FC<OrgPagesNavProps> = ({ className }) => {
152
const location = useLocation();
153
154
return (
155
<div
156
className={classNames(
157
"text-base text-gray-500 dark:text-gray-400 flex items-center space-x-1 py-1",
158
className,
159
)}
160
>
161
{leftMenu.map((entry) => (
162
<div key={entry.title}>
163
<PillMenuItem name={entry.title} selected={isSelected(entry, location)} link={entry.link} />
164
</div>
165
))}
166
</div>
167
);
168
};
169
170
type UserMenuProps = {
171
user?: User;
172
className?: string;
173
withAdminLink?: boolean;
174
onFeedback?: () => void;
175
};
176
const UserMenu: FC<UserMenuProps> = ({ user, className, withAdminLink, onFeedback }) => {
177
const { data: installationConfig, isLoading: isInstallationConfigLoading } = useInstallationConfiguration();
178
const isGitpodIo = isInstallationConfigLoading ? false : !installationConfig?.isDedicatedInstallation;
179
180
const adminSection = useMemo(() => {
181
const items: ContextMenuEntry[] = [];
182
183
if (withAdminLink && user?.rolesOrPermissions?.includes(RoleOrPermission.ADMIN)) {
184
items.push({
185
title: "Admin",
186
link: "/admin",
187
});
188
}
189
190
// Add a separator to the last item
191
if (items.length > 0) {
192
items[items.length - 1].separator = true;
193
}
194
195
return items;
196
}, [user?.rolesOrPermissions, withAdminLink]);
197
198
const menuEntries = useMemo(() => {
199
const entries: ContextMenuEntry[] = [
200
{
201
title: (user && (getPrimaryEmail(user) || user?.name)) || "User",
202
customFontStyle: "text-gray-400",
203
separator: true,
204
},
205
{
206
title: "User Settings",
207
link: "/user/settings",
208
},
209
{
210
title: "Docs",
211
href: "https://www.gitpod.io/docs/introduction",
212
target: "_blank",
213
rel: "noreferrer",
214
},
215
{
216
title: "Help",
217
href: "https://www.gitpod.io/support/",
218
target: "_blank",
219
rel: "noreferrer",
220
separator: !isGitpodIo,
221
},
222
];
223
224
if (isGitpodIo) {
225
entries.push({
226
title: "Feedback",
227
onClick: onFeedback,
228
separator: true,
229
});
230
}
231
232
entries.push(...adminSection);
233
234
entries.push({
235
title: "Log out",
236
href: gitpodHostUrl.asApiLogout().toString(),
237
});
238
239
return entries;
240
}, [adminSection, user, isGitpodIo, onFeedback]);
241
242
return (
243
<div
244
className={classNames(
245
"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",
246
className,
247
)}
248
data-analytics='{"label":"Account"}'
249
>
250
<ContextMenu menuEntries={menuEntries}>
251
<img className="rounded-full w-8 h-8" src={user?.avatarUrl || ""} alt={user?.name || "Anonymous"} />
252
</ContextMenu>
253
</div>
254
);
255
};
256
257
function isSelected(entry: Entry, location: Location<any>) {
258
const all = [entry.link, ...(entry.alternatives || [])].map((l) => l.toLowerCase());
259
const path = location.pathname.toLowerCase();
260
return all.some((n) => n === path || n + "/" === path || path.startsWith(n + "/"));
261
}
262
263