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