import { AnimatePresence, motion } from 'framer-motion' import Head from 'next/head' import { useRouter } from 'next/router' import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect, useState } from 'react' import { useParams } from 'common' import { useIsSQLEditorTabsEnabled, useIsTableEditorTabsEnabled, } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import ProjectAPIDocs from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs' import { AIAssistant } from 'components/ui/AIAssistantPanel/AIAssistant' import AISettingsModal from 'components/ui/AISettingsModal' import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import { Loading } from 'components/ui/Loading' import { ResourceExhaustionWarningBanner } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { withAuth } from 'hooks/misc/withAuth' import { PROJECT_STATUS } from 'lib/constants' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { useAppStateSnapshot } from 'state/app-state' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { cn, ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui' import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav' import EnableBranchingModal from '../AppLayout/EnableBranchingButton/EnableBranchingModal' import { useEditorType } from '../editors/EditorsLayout.hooks' import BuildingState from './BuildingState' import ConnectingState from './ConnectingState' import LoadingState from './LoadingState' import { ProjectPausedState } from './PausedState/ProjectPausedState' import PauseFailedState from './PauseFailedState' import PausingState from './PausingState' import ProductMenuBar from './ProductMenuBar' import { ResizingState } from './ResizingState' import RestartingState from './RestartingState' import RestoreFailedState from './RestoreFailedState' import RestoringState from './RestoringState' import { UpgradingState } from './UpgradingState' // [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/general', '/project/[ref]/settings/database', '/project/[ref]/settings/storage', '/project/[ref]/settings/infrastructure', '/project/[ref]/settings/addons', ] const routesToIgnoreDBConnection = [ '/project/[ref]/branches', '/project/[ref]/database/backups/scheduled', '/project/[ref]/database/backups/pitr', '/project/[ref]/settings/addons', ] const routesToIgnorePostgrestConnection = [ '/project/[ref]/reports', '/project/[ref]/settings/general', '/project/[ref]/settings/database', '/project/[ref]/settings/infrastructure', '/project/[ref]/settings/addons', ] export interface ProjectLayoutProps { title?: string isLoading?: boolean isBlocking?: boolean product?: string productMenu?: ReactNode selectedTable?: string resizableSidebar?: boolean } const ProjectLayout = forwardRef>( ( { title, isLoading = false, isBlocking = true, product = '', productMenu, children, selectedTable, resizableSidebar = false, }, ref ) => { const router = useRouter() const [isClient, setIsClient] = useState(false) const selectedOrganization = useSelectedOrganization() const selectedProject = useSelectedProject() const { editorPanel, mobileMenuOpen, showSidebar, setMobileMenuOpen } = useAppStateSnapshot() const aiSnap = useAiAssistantStateSnapshot() const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled() const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled() // For tabs preview flag logic - only conditionally collapse sidebar for table editor and sql editor if feature flags are on const editor = useEditorType() const tableEditorTabsEnabled = editor === 'table' && isTableEditorTabsEnabled const sqlEditorTabsEnabled = editor === 'sql' && isSQLEditorTabsEnabled const forceShowProductMenu = !tableEditorTabsEnabled && !sqlEditorTabsEnabled const sideBarIsOpen = forceShowProductMenu || showSidebar const projectName = selectedProject?.name const organizationName = selectedOrganization?.name const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE const showProductMenu = selectedProject ? selectedProject.status === PROJECT_STATUS.ACTIVE_HEALTHY || (selectedProject.status === PROJECT_STATUS.COMING_UP && router.pathname.includes('/project/[ref]/settings')) : true const ignorePausedState = router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings') const showPausedState = isPaused && !ignorePausedState useEffect(() => { setIsClient(true) }, []) useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.metaKey && e.key === 'i' && !e.altKey && !e.shiftKey) { aiSnap.openAssistant() e.preventDefault() e.stopPropagation() } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) // eslint-disable-next-line react-hooks/exhaustive-deps }, [aiSnap.open]) return ( <> {title ? `${title} | Supabase` : selectedTable ? `${selectedTable} | ${projectName} | ${organizationName} | Supabase` : projectName ? `${projectName} | ${organizationName} | Supabase` : organizationName ? `${organizationName} | Supabase` : 'Supabase'}
{showProductMenu && productMenu && ( {sideBarIsOpen && ( {productMenu} )} )} {showProductMenu && productMenu && sideBarIsOpen && ( )}
{showPausedState ? (
) : ( {children} )}
{isClient && (aiSnap.open || editorPanel.open) && ( <> {aiSnap.open ? ( ) : editorPanel.open ? ( ) : null} )}
{productMenu} ) } ) ProjectLayout.displayName = 'ProjectLayout' export const ProjectLayoutWithAuth = withAuth(ProjectLayout) export default ProjectLayout interface MenuBarWrapperProps { isLoading: boolean isBlocking?: boolean productMenu?: ReactNode children: ReactNode } const MenuBarWrapper = ({ isLoading, isBlocking = true, productMenu, children, }: MenuBarWrapperProps) => { const router = useRouter() const selectedProject = useSelectedProject() 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://supabase.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 selectedProject = useSelectedProject() const isSettingsPages = router.pathname.includes('/project/[ref]/settings') const isVaultPage = router.pathname === '/project/[ref]/settings/vault' const isBackupsPage = router.pathname.includes('/project/[ref]/database/backups') const requiresDbConnection: boolean = (!isSettingsPages && !routesToIgnoreDBConnection.includes(router.pathname)) || isVaultPage 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.GOING_DOWN || selectedProject?.status === PROJECT_STATUS.PAUSING const isProjectPauseFailed = selectedProject?.status === PROJECT_STATUS.PAUSE_FAILED const isProjectOffline = selectedProject?.postgrestStatus === 'OFFLINE' 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 (requiresPostgrestConnection && isProjectOffline) { return } if (requiresDbConnection && isProjectRestoring) { return } if (isProjectRestoreFailed && !isBackupsPage) { return } if (requiresDbConnection && isProjectBuilding) { return } return {children} }