import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; import isUndefined from 'lodash/isUndefined'; import PropTypes from 'prop-types'; import { css, cx } from '@leafygreen-ui/emotion'; import { useObjectDependency } from '@leafygreen-ui/hooks'; import LeafyGreenProvider, { useDarkMode, } from '@leafygreen-ui/leafygreen-provider'; import { palette } from '@leafygreen-ui/palette'; import { fontFamilies } from '@leafygreen-ui/tokens'; import defaultsDeepClone from './utils/defaultsDeepClone'; import { dataFixtures as defaultDataFixtures, hostDefaults } from './data'; import { MongoNavProvider } from './MongoNavContext'; import OnElementClickProvider from './on-element-click-provider'; import OrgNav from './org-nav'; import ProjectNav from './project-nav'; import { ActiveNavElement, DataInterface, Environment, ErrorCode, HostsInterface, Mode, MongoNavInterface, OrganizationInterface, Platform, PostBodyInterface, Product, ProjectInterface, URLS, } from './types'; const ErrorCodeMap: Record = { 401: ErrorCode.NO_AUTHORIZATION, } as const; type UseMongoNavData = Pick< MongoNavInterface, | 'mode' | 'activeOrgId' | 'activeProjectId' | 'loadData' | 'dataFixtures' | 'onSuccess' | 'onError' > & { hosts: Required>; reload: boolean; }; function useMongoNavData({ hosts, mode, activeOrgId, activeProjectId, loadData, dataFixtures, onSuccess, onError, reload, }: UseMongoNavData): DataInterface | undefined { const [data, setData] = useState(undefined); const endpointURI = `${hosts.cloud}/user/shared`; const body: PostBodyInterface | null = useObjectDependency( activeProjectId || activeOrgId ? { activeProjectId, activeOrgId } : null, ); const mergedData = useObjectDependency( mode === Mode.Dev ? defaultsDeepClone(dataFixtures, defaultDataFixtures) : null, ); const handlerRef = useRef({ onSuccess, onError }); handlerRef.current = { onSuccess, onError }; useEffect(() => { if (!loadData) { setData(undefined); } else if (mergedData) { setData(mergedData); handlerRef.current.onSuccess?.(mergedData); } else { const handleResponse = (response: Response) => { if (!response.ok) { const mappedStatus = ErrorCodeMap[response.status]; handlerRef.current.onError?.(mappedStatus); console.error(mappedStatus); } else { response .json() .then(data => { setData(data); handlerRef.current.onSuccess?.(data); }) .catch(console.error); } }; const controller = new AbortController(); const fetchProductionData = () => { const configObject: RequestInit = { credentials: 'include', mode: 'cors', method: 'GET', signal: controller.signal, }; if (body) { Object.assign(configObject, { method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json', }, }); } return fetch(endpointURI, configObject); }; fetchProductionData() .then(handleResponse) .catch(error => { if (error?.name !== 'AbortError') { console.error(error); } }); return () => { controller.abort(); }; } }, [mergedData, endpointURI, body, loadData, reload]); return data; } const navContainerStyle = css` position: relative; z-index: 0; font-family: ${fontFamilies.default}; background-color: ${palette.white}; box-shadow: unset; `; /** * # MongoNav * * MongoNav component * * ``` ``` * @param props.activeProduct Describes what product is currently active. * @param props.activeNav Determines what nav item is currently active. * @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.onOrganizationChange Callback invoked when user types into organization picker. * @param props.onProjectChange Callback invoked when user types into project picker. * @param props.urls Object to enable custom overrides for every `href` used in ``. * @param props.showProjectNav Determines whether the project navigation should be shown. * @param props.admin Describes whether or not user is an `admin`. * @param props.constructOrganizationURL Function to determine destination URL when user selects a organization from the organization picker, see also `hosts`. * @param props.constructProjectURL Function to determine destination URL when user selects a project from the project picker, see also `hosts`. * @param props.mode Describes what environment the component is being used in, defaults to `production`. * @param props.onSuccess Callback that receives the response of the fetched data, having been converted from JSON into an object. * @param props.onError Function that is passed an error code as a string, so that consuming application can handle fetch failures. * @param props.onPrem onPrem config object with three keys: enabled, version and mfa * @param props.className Applies a className to the root element * @param props.onElementClick Click EventHandler that receives a `type` as its first argument and the associated `MouseEvent` as its second. This prop provides a hook into product link and logout link clicks and allows consuming applications to handle routing internally. * @param props.activeOrgId ID for active organization, will cause a POST request to cloud to update current active organization. * @param props.activeProjectId ID for active project, will cause a POST request to cloud to update current active project. * @param props.className Applies a className to the root element * @param props.loadData Determines whether or not the component will fetch data from cloud * @param props.activePlatform Determines which platform is active * @param props.alertPollingInterval Defines interval for alert polling */ const MongoNav = React.forwardRef( ( { activeProduct, activeNav, onOrganizationChange, onProjectChange, activeOrgId, activeProjectId, className, environment = Environment.Commercial, activePlatform = Platform.Cloud, mode = Mode.Production, loadData = true, showProjectNav = true, onError = () => {}, onSuccess = () => {}, onElementClick = () => {}, onPrem = { mfa: false, enabled: false, version: '' }, alertPollingInterval = 600e3, // 10 minutes admin: adminProp, hosts: hostsProp, urls: urlsProp, constructOrganizationURL: constructOrganizationURLProp, constructProjectURL: constructProjectURLProp, dataFixtures, useAppServicesBranding = true, showUnifiedAccessManagementNav, darkMode: darkModeProp, ...rest }: MongoNavInterface, ref: React.Ref, ) => { const [isLoading, setLoading] = useState(true); const [reload, setReloadToggle] = useState(false); useImperativeHandle(ref, () => ({ reloadData: () => { setReloadToggle(curr => !curr); }, })); const { darkMode } = useDarkMode(darkModeProp); const shouldShowProjectNav = showProjectNav && !onPrem.enabled; const hosts: Required = defaultsDeepClone( hostsProp, hostDefaults(environment === Environment.Government), ); const data = useMongoNavData({ hosts, mode, activeOrgId, activeProjectId, loadData, dataFixtures, onSuccess, onError, reload, }); // When data is defined, remove the flag useEffect(() => { setLoading(isUndefined(data)); }, [data]); const admin = (adminProp ?? data?.account?.admin) === true; const currentOrgId = data?.currentOrganization?.orgId; const currentProjectId = data?.currentProject?.projectId; const defaultURLS: Omit = { mongoSelect: { viewAllProjects: `${hosts.cloud}/v2#/org/${currentOrgId}/projects`, viewAllOrganizations: `${hosts.cloud}/v2#/preferences/organizations`, newProject: `${hosts.cloud}/v2#/org/${currentOrgId}/projects/create`, orgSettings: `${hosts.cloud}/v2#/org/${currentOrgId}/settings/general`, }, orgNav: { leaf: currentOrgId ? `${hosts.cloud}/v2#/org/${currentOrgId}/` : `/`, settings: `${hosts.cloud}/v2#/org/${currentOrgId}/settings/general`, accessManager: `${hosts.cloud}/v2#/org/${currentOrgId}/access/users`, support: `${hosts.cloud}/v2#/org/${currentOrgId}/support`, newCase: `${hosts.support}/help`, docs: 'https://docs.mongodb.com', billing: `${hosts.cloud}/v2#/org/${currentOrgId}/billing/overview`, allClusters: `${hosts.cloud}/v2#/clusters`, admin: `${hosts.cloud}/v2/admin#general/overview/servers`, }, projectNav: { settings: `${hosts.cloud}/v2/${currentProjectId}#settings/groupSettings`, accessManager: `${hosts.cloud}/v2/${currentProjectId}#access`, support: `${hosts.cloud}/v2/${currentProjectId}#info/support`, integrations: `${hosts.cloud}/v2/${currentProjectId}#integrations`, alerts: `${hosts.cloud}/v2/${currentProjectId}#alerts`, activityFeed: `${hosts.cloud}/v2/${currentProjectId}#activity`, invite: `${hosts.cloud}/v2/${currentProjectId}#access/invite`, realm: `${hosts.realm}/groups/${currentProjectId}/apps`, charts: data?.currentProject?.chartsActivated ? `${hosts.cloud}/charts/${currentProjectId}` : `${hosts.cloud}/v2/${currentProjectId}#charts`, }, onPrem: { profile: `${hosts.cloud}/v2#/account/profile`, mfa: `${hosts.cloud}/v2#/account/2fa`, personalization: `${hosts.cloud}/v2#/account/personalization`, invitations: `${hosts.cloud}/v2#/account/invitations`, organizations: `${hosts.cloud}/v2#/account/organizations`, publicApiAccess: `${hosts.cloud}/v2#/account/publicApi`, featureRequest: 'https://feedback.mongodb.com', }, }; const urls: URLS = defaultsDeepClone(urlsProp, defaultURLS); const defaultOrgURL = ({ orgId }: OrganizationInterface) => `${hosts.cloud}/v2#/org/${orgId}/projects`; const constructOrganizationURL = constructOrganizationURLProp ?? defaultOrgURL; const defaultProjectURL = ({ projectId }: ProjectInterface) => `${hosts.cloud}/v2/${projectId}#`; const constructProjectURL = constructProjectURLProp ?? defaultProjectURL; const filteredProjects = data?.projects?.filter(project => { return project.orgId === data.currentProject?.orgId; }); return (
{shouldShowProjectNav && ( )}
); }, ); MongoNav.displayName = 'MongoNav'; MongoNav.propTypes = { activeProduct: PropTypes.oneOf(Object.values(Product)), activeNav: PropTypes.oneOf(Object.values(ActiveNavElement)), hosts: PropTypes.objectOf(PropTypes.string), onOrganizationChange: PropTypes.func, onProjectChange: PropTypes.func, urls: PropTypes.objectOf(PropTypes.object), showProjectNav: PropTypes.bool, admin: PropTypes.bool, constructOrganizationURL: PropTypes.func, constructProjectURL: PropTypes.func, mode: PropTypes.oneOf(Object.values(Mode)), onSuccess: PropTypes.func, onError: PropTypes.func, // @ts-expect-error onPrem: PropTypes.shape({ mfa: PropTypes.bool, enabled: PropTypes.bool, version: PropTypes.string, }), onElementClick: PropTypes.func, activeOrgId: PropTypes.string, activeProjectId: PropTypes.string, darkMode: PropTypes.bool, }; export default MongoNav;