import React, { useRef, useState } from 'react'; import defaultsDeep from 'lodash/defaultsDeep'; import PropTypes from 'prop-types'; import Badge from '@leafygreen-ui/badge'; import Button from '@leafygreen-ui/button'; import { css, cx } from '@leafygreen-ui/emotion'; // @ts-expect-error import ArrowRightIcon from '@leafygreen-ui/icon/dist/ArrowRight'; // @ts-expect-error import BellIcon from '@leafygreen-ui/icon/dist/Bell'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { Theme } from '@leafygreen-ui/lib'; import { MongoDBLogoMark } from '@leafygreen-ui/logo'; import { FocusableMenuItem, Menu, MenuItem, MenuSeparator, SubMenu, } from '@leafygreen-ui/menu'; import { Link } from '@leafygreen-ui/typography'; import { hostDefaults } from '../../data'; import { CloudIcon, DevHubIcon, DocsIcon, ForumsIcon, MegaphoneIcon, SupportIcon, UniversityIcon, } from '../../icons'; import { useOnElementClick } from '../../on-element-click-provider'; import { AccountInterface, ActiveNavElement, Environment, HostsInterface, NavElement, Platform, URLS, UserMenuURLS, } from '../../types'; import { descriptionStyle, headerBaseStyle, headerThemeStyle, linkColorStyle, logoMarkBaseStyle, logoMarkThemeStyle, menuBaseStyle, menuGlyphThemeStyle, menuThemeStyle, mfaBannerBaseStyle, mfaBannerThemeStyle, nameStyle, productLinkBaseStyle, productLinkIconStyle, productLinkThemeStyle, subMenuActiveContainerStyle, subMenuClassName, subMenuContainerBaseStyle, subMenuContainerThemeStyle, subMenuItemStyle, truncate, } from './UserMenu.style'; import { UserMenuTrigger } from './UserMenuTrigger'; type PolyProps = { as: 'a'; href: string } | { as: 'button' }; type ProductString = | 'cloud' | 'university' | 'support' | 'developer' | 'docs' | 'forums'; type DescriptionProps = { isActive: boolean; isGovernment?: boolean; theme: Theme; } & ( | { /** Required if product is not provided */ link?: string; /** Required if link is not provided */ product: ProductString; } | { /** Required if product is not provided */ link: string; /** Required if link is not provided */ product?: ProductString; } ); function UserMenuItemDescription({ product, link, isGovernment = false, theme, }: DescriptionProps) { if (!link) { if ( isGovernment && ['cloud', 'support'].includes(product as ProductString) ) { link = `${product}.mongodbgov.com`; } else { link = `${product}.mongodb.com`; } } else { link = link.replace(/https?:\/\//, ''); // replace any http in the provided link } return (
{link}
); } export interface UserMenuProps { /** * Object that contains information about the active user. * {firstName: 'string', lastName: 'string', email: 'string'} */ account?: AccountInterface; /** * Determines what nav item is currently active. */ activeNav?: NavElement; /** * Callback invoked after the user clicks log out. */ onLogout?: React.MouseEventHandler; /** * Callback invoked after the user clicks a product. */ onProductChange?: React.MouseEventHandler; /** * Object that supplies URL overrides to UserMenu component. * Shape: { userMenu:{ cloud: { userPreferences, organizations, invitations, mfa }, university: { universityPreferences }, support: { userPreferences }, account: { homepage } }} */ urls?: URLS | { userMenu: UserMenuURLS }; /** * Object that supplies host overrides to UserMenu component. * Shape: { cloud, realm, charts, account, university, support } * Defaults to the production homepages of each product */ hosts?: HostsInterface; /** * MongoDB platform that is currently active. */ activePlatform?: Platform; /** * Describes the environment that the consumer is in: `commercial` or `government` */ environment?: Environment; /** * Whether the content has been loaded */ loading?: boolean; } /** * # UserMenu * * UserMenu component * * ``` ``` * @param props.account Object that contains information about the active user. * {firstName: 'string', lastName: 'string', email: 'string'} * @param props.activeNav Determines what nav item is currently active. * @param props.onLogout Callback fired when a user logs out. * @param props.onProductChange Callback invoked after the user clicks a product. * @param props.hosts Object where keys are MDB products and values are the desired hostURL override for that product, to enable `` to work across all environments. * @param props.urls Object to enable custom overrides for every `href` used in ``. * @param props.activePlatform MongoDB platform that is currently active. * @param props.environment Describes the environment that the consumer is in: `commercial` or `government` */ function UserMenu({ account, activeNav, activePlatform, onLogout: onLogoutProp, urls: urlsProp, hosts: hostsProp, onProductChange = () => {}, environment = Environment.Commercial, loading, }: UserMenuProps) { const { theme, darkMode } = useDarkMode(); const hosts: Required = defaultsDeep( hostsProp, hostDefaults(environment === Environment.Government), ); const onElementClick = useOnElementClick(); const triggerRef = useRef(null); const onLogout = (e: React.MouseEvent) => { if (onLogoutProp) { return onLogoutProp(e); } else { return onElementClick(NavElement.Logout)(e); } }; const defaultURLs: Required = { cloud: { userPreferences: `${hosts.cloud}/v2#/preferences/personalization`, organizations: `${hosts.cloud}/v2#/preferences/organizations`, invitations: `${hosts.cloud}/v2#/preferences/invitations`, mfa: `${hosts.cloud}/v2#/preferences/2fa`, }, docs: 'https://docs.mongodb.com', university: { transcript: `${hosts.university}/learn/transcript`, faqs: `${hosts.university}/support`, }, devHub: 'https://developer.mongodb.com', forums: { forumsPreferences: 'https://www.mongodb.com/community/forums/my/preferences/account', }, support: { userPreferences: `${hosts.support}/user`, }, account: { homepage: `${hosts.account}/account/profile/overview`, }, logout: `${hosts.account}/account/login?signedOut=true`, }; const userMenuUrls: Required = defaultsDeep( urlsProp?.userMenu, defaultURLs, ); const cloudUrls = userMenuUrls.cloud; const [open, setOpen] = useState(false); const name = account ? `${account.firstName ?? ''} ${account.lastName ?? ''}` : ''; const isAccount = activePlatform === Platform.Account; const isCloud = activePlatform === Platform.Cloud; const isDocs = activePlatform === Platform.Docs; const isSupport = activePlatform === Platform.Support; const isUniversity = activePlatform === Platform.University; const isGovernment = environment === Environment.Government; const isDevHub = activePlatform === Platform.DevHub; const isForums = activePlatform === Platform.Forum; const onClick = (e: React.MouseEvent) => { onProductChange(e); setOpen(false); }; const toggleOpen = (event?: React.MouseEvent) => { // We stop the native event from bubbling, but allow the React.Synthetic event to bubble // This way click handlers on parent components will still fire, // but this click event won't propagate up to the document and immediately close the menu. event?.nativeEvent?.stopPropagation?.(); setOpen(curr => !curr); }; const sharedProps = { target: '_blank', rel: 'noopener noreferrer', }; const feedbackAnchorProps = { href: 'https://feedback.mongodb.com/', target: '_blank', rel: 'noopener noreferrer', }; const ariaHiddenProps = { 'aria-hidden': true, alt: '', role: 'presentation', }; const universitySharedProps = { size: 'large', active: isUniversity, disabled: !account, href: hosts.university, ['data-testid']: 'user-submenu-university', glyph: ( ), onClick: onElementClick(NavElement.UserMenuUniversity, onClick), className: cx( subMenuClassName, subMenuContainerBaseStyle, subMenuContainerThemeStyle[theme], { [subMenuActiveContainerStyle]: isUniversity, }, ), description: ( ), } as const; const getItemProps = (href: string | undefined): PolyProps => { if (href) { return { href, as: 'a' } as PolyProps; } return { as: 'button' } as PolyProps; }; return ( <> {account?.shouldSeeAccountMfaBanner && (
  • MFA is now available for your Account!
  • )}
  • {name}

    {account?.email ?? ''}

  • } onClick={onElementClick(NavElement.UserMenuCloud, onClick)} description={ } className={cx( subMenuClassName, subMenuContainerBaseStyle, subMenuContainerThemeStyle[theme], { [subMenuActiveContainerStyle]: isCloud, }, )} > User Preferences Invitations {(account?.openInvitations ?? 0) > 0 && ( {account?.openInvitations} )} Organizations {!isGovernment && account?.hasLegacy2fa && ( Legacy 2FA )} } onClick={onElementClick(NavElement.UserMenuDocs, onClick)} className={cx( subMenuClassName, subMenuContainerBaseStyle, subMenuContainerThemeStyle[theme], { [subMenuActiveContainerStyle]: isDocs, }, )} description={ } > Documentation {isUniversity ? ( Transcript FAQ ) : ( University )} } title="Forums" onClick={onElementClick(NavElement.UserMenuForums, onClick)} className={cx( subMenuClassName, subMenuContainerBaseStyle, subMenuContainerThemeStyle[theme], { [subMenuActiveContainerStyle]: isForums, }, )} description={ } > Forum Preferences } onClick={onElementClick(NavElement.UserMenuDevHub, onClick)} className={cx( subMenuClassName, subMenuContainerBaseStyle, subMenuContainerThemeStyle[theme], { [subMenuActiveContainerStyle]: isDevHub, }, )} description={ } > Developer Center } onClick={onElementClick(NavElement.UserMenuSupport, onClick)} description={ } className={cx( subMenuClassName, subMenuContainerBaseStyle, subMenuContainerThemeStyle[theme], { [subMenuActiveContainerStyle]: isSupport, }, )} > User Preferences } data-testid="user-menuitem-feedback" onClick={onElementClick(NavElement.UserMenuFeedback)} className={subMenuContainerThemeStyle[theme]} > {isGovernment ? 'Feature Requests' : 'Give us feedback'} Log out
    ); } UserMenu.displayName = 'UserMenu'; UserMenu.propTypes = { user: PropTypes.objectOf(PropTypes.string), activePlatform: PropTypes.oneOf([ 'account', 'cloud', 'support', 'university', 'devHub', 'forums', ]), onLogout: PropTypes.func, onProductChange: PropTypes.func, onAccountClick: PropTypes.func, }; export default UserMenu;