mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
## What kind of change does this PR introduce? UI improvements. - Resolves DEPR-401 - Resolves DEPR-424 - Resolves DEPR-425 ## What is the current behaviour? Studio currently blurs two different concepts together: - `billing_partner` / `billing_via_partner`, which represent real billing ownership for marketplace-managed organisations such as AWS and Vercel - Stripe connection state, which is not actually partner billing in the same sense, but was previously being mocked through the same UI paths That made the Stripe work harder to reason about and left some local behaviour dependent on temporary overrides rather than the API shape we want to ship. ## What is the new behaviour? This PR separates those concerns while keeping the existing AWS and Vercel marketplace experience intact. - AWS and Vercel continue to use `billing_partner` / `billing_via_partner` for billing ownership, lockouts, and manage CTAs - Stripe display state now comes from `integration_source`, which lets Studio show Stripe-specific badges and alerts without treating Stripe as a billing partner - organisation-level partner UI is unified across AWS, Vercel, and Stripe, including the org banner, navbar icon treatment, and organisation cards - project-level Stripe UI now appears only when the project itself is marked as Stripe-connected, including the project switcher, project list surfaces, and a project-level banner - Stripe-connected organisations are no longer incorrectly blocked behind the AWS/Vercel-style billing management alerts for invoices, billing address, payment methods, or plan changes - banner dismissal is scoped to the relevant org/project and partner/integration state ## Review order Most of the diff size here is regression tests and generated types. The behavioural changes are concentrated in a smaller set of files. Recommended review order: 1. `integration_source` vs `billing_partner` data-model split and org/project query mapping 2. org-level UI: partner icon, org banner, org dropdown/card treatment 3. billing gating updates for Stripe vs AWS/Vercel 4. project-level Stripe UI: dropdown, list surfaces, banner 5. tests and generated types | Stripe Org(s) | | --- | | <img width="1024" height="759" alt="Organizations Supabase" src="https://github.com/user-attachments/assets/d0ef338c-3b41-4c6d-b3bd-f21a2c182840" /> | | Vercel Org(s) | | --- | | <img width="1024" height="759" alt="Organizations Supabase" src="https://github.com/user-attachments/assets/1dc57770-3f24-45ac-840f-34680555cde8" /> | | AWS Org(s) | | --- | | <img width="1024" height="759" alt="Organizations Supabase" src="https://github.com/user-attachments/assets/7847dad0-ee30-4a65-ab0b-b3b16af0d34f" /> | | Stripe Org, Non-Stripe Project | | --- | | <img width="1152" height="885" alt="Mallet Toolshed Supabase-1673E019-792C-462C-B6F8-C5DDB810B331" src="https://github.com/user-attachments/assets/556fbea3-b5ae-4f2f-96b9-6f66c6654e4a" /> | | Stripe Org, Stripe Project | | --- | | <img width="1152" height="885" alt="Hammer Toolshed Supabase-7E86C17C-561F-4221-BD16-EAFF7D41AAE0" src="https://github.com/user-attachments/assets/94f8daf6-0320-413e-8d56-59f9acaaea15" /> | | Vercel Org | | --- | | <img width="1024" height="759" alt="Projects Toolshed Supabase-A7891653-9366-4B99-89DD-789D70CD52E3" src="https://github.com/user-attachments/assets/c87ee6e8-4451-4866-a905-23a38b2593e3" /> | | AWS Org | | --- | | <img width="1024" height="759" alt="Projects Toolshed Supabase-58A43ECE-569E-4541-9463-346A90B02CFF" src="https://github.com/user-attachments/assets/9350a180-4d58-42a1-ad1a-95893c2e8b12" /> | This also removes the old Stripe mock override path in Studio so the frontend matches the intended API model more closely. ## ~~Dependencies~~ (merged!) This work depends on the private platform change that exposes `integration_source` on the relevant organisation and project payloads: - https://github.com/supabase/platform/pull/31874 _Update: now merged._ ## Local testing ### Stripe If you have the private `platform` repo checked out locally, make sure your local API returns `integration_source: 'stripe_projects'` consistently for the Stripe-linked org/project you are testing. Important responses: - `/platform/organizations` - `/platform/organizations/:slug/projects` - `/platform/projects/:ref` Verify: - org banner and org icon show the Stripe connected state - unopened and opened project switcher both show Stripe only for Stripe-linked projects - project cards / table rows show the Stripe chip only for Stripe-linked projects - the project-level Stripe banner appears across project surfaces - billing address, tax ID, invoices, payment methods, and plan changes remain editable in Studio for Stripe orgs ### Vercel Use a Vercel Marketplace org with real `billing_partner` / `billing_via_partner` values. Important org-level endpoints for local mocking in `platform`: - `/platform/organizations` - `/platform/organizations/:slug` - `/platform/organizations/:slug/billing/subscription` Project-level Vercel indicators still come from `/platform/integrations/:slug`, not `integration_source`. ### AWS Use an AWS Marketplace org with real `billing_partner` / `billing_via_partner` values. Important org-level endpoints for local mocking in `platform`: - `/platform/organizations` - `/platform/organizations/:slug` - `/platform/organizations/:slug/billing/subscription` AWS does not currently have a Stripe-like project-level indicator in these org/project payloads. ## Notes - `billing_partner` is no longer the right abstraction for the Stripe-connected case in this PR. It remains the source of truth for marketplace billing ownership, while Stripe currently uses `integration_source` as a connection/display signal. - I re-ran `pnpm api:codegen` while tightening this PR and kept only the generated type changes this branch actually depends on, to avoid unrelated API drift in the review. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Stripe Projects integration added for billing and project flows * Partner icons/badges shown across org and project lists, dropdowns, and rows * Dismissible, partner-specific marketplace/integration banners with contextual CTA behavior * Improved partner-billing detection to drive billing UI and invoice/plan availability * **Tests** * Extensive new test coverage for billing UI, partner-managed fallbacks, banners, icons, and related flows <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
494 lines
18 KiB
TypeScript
494 lines
18 KiB
TypeScript
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_Shadcn_,
|
|
AlertDescription_Shadcn_,
|
|
AlertTitle_Shadcn_,
|
|
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<HTMLDivElement, PropsWithChildren<ProjectLayoutProps>>(
|
|
(
|
|
{
|
|
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 || '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,
|
|
])
|
|
|
|
useLayoutEffect(() => {
|
|
const unregister = registerOpenMenu(() => {
|
|
setMobileSheetContent(
|
|
<MobileMenuContent
|
|
currentProductMenu={productMenu ?? null}
|
|
currentProduct={product}
|
|
currentSectionKey={currentSectionKey}
|
|
onCloseSheet={() => setMobileSheetContent(null)}
|
|
/>
|
|
)
|
|
})
|
|
return unregister
|
|
}, [registerOpenMenu, productMenu, product, currentSectionKey, setMobileSheetContent])
|
|
|
|
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"
|
|
disabled={!resizableSidebar}
|
|
>
|
|
<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} 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_Shadcn_
|
|
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_Shadcn_>This project is connected to Stripe</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
Changes made here may affect your connected Stripe project.
|
|
</AlertDescription_Shadcn_>
|
|
</div>
|
|
<ButtonTooltip
|
|
type="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_Shadcn_>
|
|
)}
|
|
{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 isLoading={isLoading} isBlocking={isBlocking}>
|
|
<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 {
|
|
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 { 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]') ? <LoadingState /> : <LogoLoader />
|
|
}
|
|
|
|
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>
|
|
}
|