import { LOCAL_STORAGE_KEYS, mergeRefs, useParams } from 'common' import { AnimatePresence, motion } from 'framer-motion' import { XIcon } from 'lucide-react' import Head from 'next/head' import { useRouter } from 'next/router' import { forwardRef, Fragment, useEffect, useLayoutEffect, type PropsWithChildren, type ReactNode, } from 'react' import { Alert, AlertDescription, AlertTitle, cn, LogoLoader, ResizableHandle, ResizablePanel, ResizablePanelGroup, useIsMobile, usePanelRef, } from 'ui' import { useEditorType } from '../editors/EditorsLayout.hooks' import { useSetMainScrollContainer } from '../MainScrollContainerContext' import { useMobileSheet } from '../Navigation/NavigationBar/MobileSheetContext' import ProductMenuBar from '../Navigation/ProductMenuBar' import BuildingState from './BuildingState' import ConnectingState from './ConnectingState' import { getSectionKeyFromPathname, MobileMenuContent } from './LayoutHeader/MobileMenuContent' import { LoadingState } from './LoadingState' import { ProjectPausedState } from './PausedState/ProjectPausedState' import { PauseFailedState } from './PauseFailedState' import { PausingState } from './PausingState' import { ResizingState } from './ResizingState' import RestartingState from './RestartingState' import { RestoreFailedState } from './RestoreFailedState' import { RestoringState } from './RestoringState' import { UnhealthyState } from './UnhealthyState' import { UpgradingState } from './UpgradingState' import { CreateBranchModal } from '@/components/interfaces/BranchManagement/CreateBranchModal' import { ProjectAPIDocs } from '@/components/interfaces/ProjectAPIDocs/ProjectAPIDocs' import { BannerFreeMicroUpgrade } from '@/components/ui/BannerStack/Banners/BannerFreeMicroUpgrade' import { BANNER_ID, useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import PartnerIcon from '@/components/ui/PartnerIcon' import { ResourceExhaustionWarningBanner } from '@/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner' import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query' import { useCustomContent } from '@/hooks/custom-content/useCustomContent' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { withAuth } from '@/hooks/misc/withAuth' import { PROJECT_STATUS } from '@/lib/constants' import { MANAGED_BY } from '@/lib/constants/infrastructure' import { buildStudioPageTitle } from '@/lib/page-title' import { getPathnameWithoutQuery } from '@/lib/pathname.utils' import { useAppStateSnapshot } from '@/state/app-state' import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' // [Joshen] This is temporary while we unblock users from managing their project // if their project is not responding well for any reason. Eventually needs a bit of an overhaul const routesToIgnoreProjectDetailsRequest = [ '/project/[ref]/settings/infrastructure', '/project/[ref]/settings/addons', '/project/[ref]/settings/general', '/project/[ref]/database/settings', '/project/[ref]/storage/settings', ] const routesToIgnoreDBConnection = [ '/project/[ref]/branches', '/project/[ref]/database/backups', '/project/[ref]/settings', '/project/[ref]/functions', '/project/[ref]/logs', ] const routesToIgnorePostgrestConnection = [ '/project/[ref]/settings/general', '/project/[ref]/settings/infrastructure', '/project/[ref]/settings/addons', '/project/[ref]/database/settings', '/project/[ref]/reports', ] const DEFAULT_PROJECT_INTEGRATION_BANNER_DISMISS_KEY = LOCAL_STORAGE_KEYS.PROJECT_INTEGRATION_BANNER_DISMISSED('unknown', 'unknown') function getProjectIntegrationBannerDismissKey({ projectRef, integrationSource, }: { projectRef?: string integrationSource?: string | null }) { if (!projectRef || !integrationSource) return DEFAULT_PROJECT_INTEGRATION_BANNER_DISMISS_KEY return LOCAL_STORAGE_KEYS.PROJECT_INTEGRATION_BANNER_DISMISSED(projectRef, integrationSource) } export interface ProjectLayoutProps { isLoading?: boolean isBlocking?: boolean product?: string productMenu?: ReactNode browserTitle?: { entity?: string section?: string override?: string } // Deprecated: use browserTitle.entity instead. Kept for backwards compatibility. selectedTable?: string resizableSidebar?: boolean productMenuClassName?: string } export const ProjectLayout = forwardRef>( ( { isLoading = false, isBlocking = true, product = '', productMenu, browserTitle, children, selectedTable, resizableSidebar = false, productMenuClassName, }, ref ) => { const router = useRouter() const { data: selectedOrganization } = useSelectedOrganizationQuery() const { data: selectedProject } = useSelectedProjectQuery() const { addBanner, dismissBanner } = useBannerStack() const { data: resourceWarnings } = useResourceWarningsQuery({ slug: selectedOrganization?.slug, }) const projectResourceWarnings = resourceWarnings?.find( (w) => w.project === selectedProject?.ref ) const isComputeNearExhaustion = !!projectResourceWarnings?.cpu_exhaustion || !!projectResourceWarnings?.memory_and_swap_exhaustion || !!projectResourceWarnings?.disk_space_exhaustion || !!projectResourceWarnings?.disk_io_exhaustion const isNanoCompute = selectedProject?.infra_compute_size === 'nano' const showUpgradeBanner = isNanoCompute && isComputeNearExhaustion const [isFreeMicroUpgradeBannerDismissed] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.FREE_MICRO_UPGRADE_BANNER_DISMISSED(selectedProject?.ref ?? ''), false ) const [isProjectIntegrationBannerDismissed, setIsProjectIntegrationBannerDismissed] = useLocalStorageQuery( getProjectIntegrationBannerDismissKey({ projectRef: selectedProject?.ref, integrationSource: selectedProject?.integration_source, }), false ) const { showSidebar } = useAppStateSnapshot() const { setContent: setMobileSheetContent, registerOpenMenu } = useMobileSheet() const pathname = getPathnameWithoutQuery(router.asPath, router.pathname) const currentSectionKey = getSectionKeyFromPathname(pathname) const setMainScrollContainer = useSetMainScrollContainer() const combinedRef = mergeRefs(ref, setMainScrollContainer) const { appTitle } = useCustomContent(['app:title']) const brandTitle = appTitle || 'Briven' const isMobile = useIsMobile() const editor = useEditorType() const forceShowProductMenu = editor === undefined const sideBarIsOpen = (forceShowProductMenu || showSidebar) && !isMobile const panelRef = usePanelRef() const projectName = selectedProject?.name const organizationName = selectedOrganization?.name const pageTitle = browserTitle?.override || buildStudioPageTitle({ entity: browserTitle?.entity ?? selectedTable, section: browserTitle?.section, surface: product, project: projectName, org: organizationName, brand: brandTitle, }) || brandTitle const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE const ignorePausedState = router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings') || router.pathname.includes('/project/[ref]/functions') || router.pathname.includes('/project/[ref]/logs') const showPausedState = isPaused && !ignorePausedState const showStripeProjectBanner = selectedProject?.integration_source === 'stripe_projects' && !isProjectIntegrationBannerDismissed useEffect(() => { if (!selectedProject?.ref) return const isProjectHomepage = router.pathname === '/project/[ref]' if (isProjectHomepage && showUpgradeBanner && !isFreeMicroUpgradeBannerDismissed) { addBanner({ id: BANNER_ID.FREE_MICRO_UPGRADE, isDismissed: false, content: , priority: 2, }) } else { dismissBanner(BANNER_ID.FREE_MICRO_UPGRADE) } }, [ router.pathname, selectedProject?.ref, showUpgradeBanner, isFreeMicroUpgradeBannerDismissed, addBanner, dismissBanner, ]) useLayoutEffect(() => { const unregister = registerOpenMenu(() => { setMobileSheetContent( setMobileSheetContent(null)} /> ) }) return unregister }, [registerOpenMenu, productMenu, product, currentSectionKey, setMobileSheetContent]) return ( <> {pageTitle}
{productMenu && sideBarIsOpen && ( {productMenu} )} {productMenu && sideBarIsOpen && ( )}
{showStripeProjectBanner && (
This project is connected to Stripe Changes made here may affect your connected Stripe project.
} className="h-7 w-7 p-0" onClick={() => setIsProjectIntegrationBannerDismissed(true)} aria-label="Dismiss project integration banner" tooltip={{ content: { text: 'Dismiss' } }} />
)} {showPausedState ? (
) : ( {children} )}
) } ) ProjectLayout.displayName = 'ProjectLayout' export const ProjectLayoutWithAuth = withAuth(ProjectLayout) interface MenuBarWrapperProps { isLoading: boolean isBlocking?: boolean productMenu?: ReactNode children: ReactNode } const MenuBarWrapper = ({ isLoading, isBlocking = true, productMenu, children, }: MenuBarWrapperProps) => { const router = useRouter() const { data: selectedProject } = useSelectedProjectQuery() const requiresProjectDetails = !routesToIgnoreProjectDetailsRequest.includes(router.pathname) if (!isBlocking) { return children } const showMenuBar = !requiresProjectDetails || (requiresProjectDetails && selectedProject !== undefined) return !isLoading && productMenu && showMenuBar ? children : null } interface ContentWrapperProps { isLoading: boolean isBlocking?: boolean children: ReactNode } /** * Check project.status to show building state or error state * * [Joshen] As of 210422: Current testing connection by pinging postgres * Ideally we'd have a more specific monitoring of the project such as during restarts * But that will come later: https://briven.slack.com/archives/C01D6TWFFFW/p1650427619665549 * * Just note that this logic does not differentiate between a "restarting" state and * a "something is wrong and can't connect to project" state. * * [TODO] Next iteration should scrape long polling and just listen to the project's status */ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapperProps) => { const router = useRouter() const { ref } = useParams() const state = useDatabaseSelectorStateSnapshot() const { data: selectedProject } = useSelectedProjectQuery() const isBackupsPage = router.pathname.includes('/project/[ref]/database/backups') const isHomePage = router.pathname === '/project/[ref]' const requiresDbConnection = !routesToIgnoreDBConnection.some((x) => router.pathname.includes(x)) const requiresPostgrestConnection = !routesToIgnorePostgrestConnection.includes(router.pathname) const requiresProjectDetails = !routesToIgnoreProjectDetailsRequest.includes(router.pathname) const isRestarting = selectedProject?.status === PROJECT_STATUS.RESTARTING const isResizing = selectedProject?.status === PROJECT_STATUS.RESIZING const isProjectUpgrading = selectedProject?.status === PROJECT_STATUS.UPGRADING const isProjectRestoring = selectedProject?.status === PROJECT_STATUS.RESTORING const isProjectRestoreFailed = selectedProject?.status === PROJECT_STATUS.RESTORE_FAILED const isProjectBuilding = selectedProject?.status === PROJECT_STATUS.COMING_UP || selectedProject?.status === PROJECT_STATUS.UNKNOWN const isProjectPausing = selectedProject?.status === PROJECT_STATUS.PAUSING const isProjectPauseFailed = selectedProject?.status === PROJECT_STATUS.PAUSE_FAILED const isProjectUnhealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_UNHEALTHY const isProjectOffline = selectedProject?.postgrestStatus === 'OFFLINE' const ignoreUnhealthyState = isHomePage || router.pathname.includes('/project/[ref]/settings') || router.pathname.includes('/project/[ref]/logs') const shouldRedirectToHomeForBuilding = isProjectBuilding && requiresDbConnection && !isHomePage // Don't show building state on the home page — it handles building state inline const shouldShowBuildingState = isProjectBuilding && requiresDbConnection && !isHomePage useEffect(() => { if (shouldRedirectToHomeForBuilding && ref) { router.replace(`/project/${ref}`) } }, [shouldRedirectToHomeForBuilding, ref, router]) useEffect(() => { if (ref) state.setSelectedDatabaseId(ref) }, [ref]) if (isBlocking && (isLoading || (requiresProjectDetails && selectedProject === undefined))) { return router.pathname.endsWith('[ref]') ? : } if (isRestarting && !isBackupsPage) { return } if (isResizing && !isBackupsPage) { return } if (isProjectUpgrading && !isBackupsPage) { return } if (isProjectPausing) { return } if (isProjectPauseFailed) { return } if (isProjectUnhealthy && !ignoreUnhealthyState) { return } if (requiresPostgrestConnection && isProjectOffline) { return } if (requiresDbConnection && isProjectRestoring) { return } if (requiresDbConnection && isProjectRestoreFailed) { return } if (shouldRedirectToHomeForBuilding) { return } if (shouldShowBuildingState) { return } return {children} }