mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 04:15:04 +03:00
Project sidebars now expose enabled navigation on fixed-width pages, and the mobile project menu has an accessible dialog name. Fixed sidebar sizing and editor resize controls remain intact. **Changed:** - Remove the disabled state from the shared sidebar panel while retaining 256px fixed sizing, the disabled resize handle, and existing editor resize limits. - Add a screen-reader-only Project menu title inside the mobile project sheet without changing other sheets or their titles. **Added:** - Tests using the actual resizable wrappers for enabled navigation and handle semantics, and the actual mobile sheet for its accessible name through menu navigation. ## To test - Navigate through Database and Settings sidebar links with ordinary clicks and Tab/Enter. Enabled links should have no disabled ancestor. - Confirm fixed sidebars remain 256px wide, including an ordinary drag of their disabled handle. - In SQL Editor, verify keyboard resizing stays within 256–512px and collapse/expand still works; restore the original width and visibility without editing or executing anything. - Open the mobile project menu. Confirm its dialog name is Project menu, navigate through Tables, and close it without a missing-title warning. Restore the viewport. Validation: real regressions reproduce disabled navigation inheritance and the unnamed mobile dialog before their fixes. All 26 focused layout/menu/utility tests and source checks passed. Initial full browser checks passed normal mouse/keyboard navigation, fixed-handle drag resistance, editor resize limits, collapse/expand, and mobile navigation, with viewport/state restored and no backend writes. Both Next and TanStack production rebuilds passed. The focused mobile retest passed the linked Project menu title through sections, closing, navigation, and reopening with no new missing-title warnings, followed by desktop sidebar and editor controls; viewport, width, and visibility were restored. Unrelated development React mount/ref warnings remain separately recorded; native Next local browser and catalog data were not verified. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Project sidebar links remain available when sidebar resizing is turned off; the resize handle stays unavailable in that setting. * The mobile project menu retains the “Project menu” name as you navigate between sections. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
542 lines
19 KiB
TypeScript
542 lines
19 KiB
TypeScript
import { LOCAL_STORAGE_KEYS, mergeRefs, useFlag, 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,
|
|
isValidElement,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useRef,
|
|
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 { useMainScrollContainer, 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 { 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 { BannerExplorer } from '@/components/ui/BannerStack/Banners/BannerExplorer'
|
|
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
|
|
productMenuBadge?: ReactNode
|
|
productMenuHeader?: ReactNode
|
|
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<HTMLDivElement, PropsWithChildren<ProjectLayoutProps>>(
|
|
(
|
|
{
|
|
isLoading = false,
|
|
isBlocking = true,
|
|
product = '',
|
|
productMenuBadge,
|
|
productMenuHeader,
|
|
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 = (Array.isArray(resourceWarnings) ? 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 isExplorerEnabled = useFlag('explorer')
|
|
|
|
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 [isExplorerBannerDismissed, , { isSuccess: isLocalStorageReady }] = useLocalStorageQuery(
|
|
LOCAL_STORAGE_KEYS.EXPLORER_BANNER_DISMISSED,
|
|
false
|
|
)
|
|
|
|
const { showSidebar } = useAppStateSnapshot()
|
|
const {
|
|
content: mobileSheetContent,
|
|
setContent: setMobileSheetContent,
|
|
registerOpenMenu,
|
|
} = useMobileSheet()
|
|
|
|
const pathname = getPathnameWithoutQuery(router.asPath, router.pathname)
|
|
const currentSectionKey = getSectionKeyFromPathname(pathname)
|
|
|
|
const mainScrollContainer = useMainScrollContainer()
|
|
const setMainScrollContainer = useSetMainScrollContainer()
|
|
const combinedRef = mergeRefs(ref, setMainScrollContainer)
|
|
|
|
const { appTitle } = useCustomContent(['app:title'])
|
|
const brandTitle = appTitle || 'Supabase'
|
|
|
|
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: <BannerFreeMicroUpgrade />,
|
|
priority: 2,
|
|
})
|
|
} else {
|
|
dismissBanner(BANNER_ID.FREE_MICRO_UPGRADE)
|
|
}
|
|
}, [
|
|
router.pathname,
|
|
selectedProject?.ref,
|
|
showUpgradeBanner,
|
|
isFreeMicroUpgradeBannerDismissed,
|
|
addBanner,
|
|
dismissBanner,
|
|
])
|
|
|
|
useEffect(() => {
|
|
if (!isExplorerEnabled || !isLocalStorageReady || isExplorerBannerDismissed) return
|
|
|
|
addBanner({
|
|
id: 'explorer-banner',
|
|
priority: 2,
|
|
isDismissed: false,
|
|
content: <BannerExplorer />,
|
|
})
|
|
}, [addBanner, isExplorerEnabled, isExplorerBannerDismissed, isLocalStorageReady])
|
|
|
|
const mobileSheetContentRef = useRef(mobileSheetContent)
|
|
useLayoutEffect(() => {
|
|
mobileSheetContentRef.current = mobileSheetContent
|
|
}, [mobileSheetContent])
|
|
|
|
useLayoutEffect(() => {
|
|
const menu = (
|
|
<MobileMenuContent
|
|
currentProductMenu={productMenu ?? null}
|
|
currentProductMenuHeader={productMenuHeader}
|
|
currentProduct={product}
|
|
currentSectionKey={currentSectionKey}
|
|
onCloseSheet={() => setMobileSheetContent(null)}
|
|
/>
|
|
)
|
|
const unregister = registerOpenMenu(() => setMobileSheetContent(menu))
|
|
// Keep resource navigation in an already open sheet in sync with the layout.
|
|
const currentContent = mobileSheetContentRef.current
|
|
if (isValidElement(currentContent) && currentContent.type === MobileMenuContent) {
|
|
setMobileSheetContent(menu)
|
|
}
|
|
return unregister
|
|
}, [
|
|
registerOpenMenu,
|
|
productMenu,
|
|
productMenuHeader,
|
|
product,
|
|
currentSectionKey,
|
|
setMobileSheetContent,
|
|
])
|
|
|
|
useLayoutEffect(() => {
|
|
mainScrollContainer?.scrollTo({ top: 0, left: 0 })
|
|
}, [pathname, mainScrollContainer])
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>{pageTitle}</title>
|
|
<meta name="description" content="Supabase Studio" />
|
|
</Head>
|
|
<div className="flex flex-row h-full w-full">
|
|
<ResizablePanelGroup orientation="horizontal">
|
|
{productMenu && sideBarIsOpen && (
|
|
<ResizablePanel
|
|
panelRef={panelRef}
|
|
minSize={256}
|
|
maxSize={resizableSidebar ? 512 : 256}
|
|
defaultSize={256}
|
|
id="panel-left"
|
|
>
|
|
<AnimatePresence initial={false}>
|
|
<motion.div
|
|
initial={{ width: 0, opacity: 0, height: '100%' }}
|
|
animate={{ width: 'auto', opacity: 1, height: '100%' }}
|
|
exit={{ width: 0, opacity: 0, height: '100%' }}
|
|
className="h-full"
|
|
transition={{ duration: 0.12 }}
|
|
>
|
|
<MenuBarWrapper
|
|
isLoading={isLoading}
|
|
isBlocking={isBlocking}
|
|
productMenu={productMenu}
|
|
>
|
|
<ProductMenuBar
|
|
title={product}
|
|
titleBadge={productMenuBadge}
|
|
header={productMenuHeader}
|
|
className={productMenuClassName}
|
|
>
|
|
{productMenu}
|
|
</ProductMenuBar>
|
|
</MenuBarWrapper>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</ResizablePanel>
|
|
)}
|
|
{productMenu && sideBarIsOpen && (
|
|
<ResizableHandle
|
|
withHandle
|
|
disabled={resizableSidebar ? false : true}
|
|
className="hidden md:flex"
|
|
/>
|
|
)}
|
|
<ResizablePanel
|
|
className={cn('h-full flex flex-col w-full xl:min-w-[600px] bg-dash-sidebar')}
|
|
id="panel-project-content"
|
|
>
|
|
<main
|
|
className="h-full flex flex-col flex-1 w-full overflow-y-auto overflow-x-hidden @container"
|
|
ref={combinedRef}
|
|
>
|
|
{showStripeProjectBanner && (
|
|
<Alert
|
|
variant="default"
|
|
className="flex items-center gap-4 border-t-0 border-x-0 rounded-none"
|
|
>
|
|
<PartnerIcon
|
|
organization={{ managed_by: MANAGED_BY.STRIPE_PROJECTS }}
|
|
showTooltip={false}
|
|
size="medium"
|
|
/>
|
|
<div className="flex-1">
|
|
<AlertTitle>This project is connected to Stripe</AlertTitle>
|
|
<AlertDescription>
|
|
Changes made here may affect your connected Stripe project.
|
|
</AlertDescription>
|
|
</div>
|
|
<ButtonTooltip
|
|
variant="text"
|
|
icon={<XIcon size={14} />}
|
|
className="h-7 w-7 p-0"
|
|
onClick={() => setIsProjectIntegrationBannerDismissed(true)}
|
|
aria-label="Dismiss project integration banner"
|
|
tooltip={{ content: { text: 'Dismiss' } }}
|
|
/>
|
|
</Alert>
|
|
)}
|
|
{showPausedState ? (
|
|
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center px-4">
|
|
<div className="w-full">
|
|
<ProjectPausedState product={product} />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<ContentWrapper>
|
|
<ResourceExhaustionWarningBanner />
|
|
{children}
|
|
</ContentWrapper>
|
|
)}
|
|
</main>
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</div>
|
|
<CreateBranchModal />
|
|
<ProjectAPIDocs />
|
|
</>
|
|
)
|
|
}
|
|
)
|
|
|
|
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 {
|
|
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 = ({ 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 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 (isRestarting && !isBackupsPage) {
|
|
return <RestartingState />
|
|
}
|
|
|
|
if (isResizing && !isBackupsPage) {
|
|
return <ResizingState />
|
|
}
|
|
|
|
if (isProjectUpgrading && !isBackupsPage) {
|
|
return <UpgradingState />
|
|
}
|
|
|
|
if (isProjectPausing) {
|
|
return <PausingState project={selectedProject} />
|
|
}
|
|
|
|
if (isProjectPauseFailed) {
|
|
return <PauseFailedState />
|
|
}
|
|
|
|
if (isProjectUnhealthy && !ignoreUnhealthyState) {
|
|
return <UnhealthyState />
|
|
}
|
|
|
|
if (requiresPostgrestConnection && isProjectOffline) {
|
|
return <ConnectingState project={selectedProject} />
|
|
}
|
|
|
|
if (requiresDbConnection && isProjectRestoring) {
|
|
return <RestoringState />
|
|
}
|
|
|
|
if (requiresDbConnection && isProjectRestoreFailed) {
|
|
return <RestoreFailedState />
|
|
}
|
|
|
|
if (shouldRedirectToHomeForBuilding) {
|
|
return <LogoLoader />
|
|
}
|
|
|
|
if (shouldShowBuildingState) {
|
|
return <BuildingState />
|
|
}
|
|
|
|
return <Fragment key={selectedProject?.ref}>{children}</Fragment>
|
|
}
|