update branch

This commit is contained in:
Francesco Sansalvadore committed 2026-04-02 12:37:39 +02:00
1 parent d2951d9189
commit 5591c35f6b
28 files changed
+254 -393

No files matched your search

@@ -3,6 +3,7 @@ import Link from 'next/link'
import { PropsWithChildren, ReactNode } from 'react'
import { cn, Menu } from 'ui'
import { NavUser } from '../NavigationV2/NavUser'
import type { SidebarSection } from './AccountLayout.types'
import { useAppStateSnapshot } from '@/state/app-state'
@@ -64,79 +65,78 @@ export const SidebarContent = ({
className,
}: PropsWithChildren<Omit<WithSidebarProps, 'breadcrumbs'>> & { className?: string }) => {
return (
<>
<div
id="with-sidebar"
className={cn(
'h-full bg-dash-sidebar flex flex-col justify-between',
'hide-scrollbar w-full md:w-64 md:border-r border-default',
className
)}
>
<div className="flex-1 flex flex-col">
{backToDashboardURL && (
<div className="flex-shrink-0 hidden md:block">
<div className="flex h-12 max-h-12 items-center border-b px-6 border-default">
<Link
href={backToDashboardURL}
className="flex text-sm flex-row gap-2 items-center text-foreground-lighter focus-visible:text-foreground hover:text-foreground"
>
<ArrowLeft strokeWidth={1.5} size={16} />
Back to dashboard
</Link>
</div>
</div>
)}
{header && header}
<div className="flex-1 overflow-auto">
<div className="flex flex-col space-y-8">
<Menu type="pills">
{customSidebarContent}
{sections.map((section, idx) => (
<div key={section.key || section.heading}>
{Boolean(section.heading) ? (
<SectionWithHeaders
key={section.key}
section={section}
subitems={subitems}
subitemsParentKey={subitemsParentKey}
/>
) : (
<div className="my-6 space-y-8">
<div className="mx-3">
{section.links.map((link, i: number) => {
const isActive = link.isActive && !subitems
return (
<Menu.Item
key={`${link.key}-${i}`}
rounded
active={isActive}
icon={link.icon}
>
<Link href={link.href || ''} className="block">
<div className="flex w-full items-center justify-between gap-1">
<div className="flex items-center gap-2 truncate w-full">
<span className="truncate">{link.label}</span>
</div>
</div>
</Link>
</Menu.Item>
)
})}
</div>
</div>
)}
{idx !== sections.length - 1 && (
<div className="h-px w-full bg-border-overlay" />
)}
</div>
))}
</Menu>
<div
id="with-sidebar"
className={cn(
'h-full bg-dash-sidebar flex flex-col justify-between',
'hide-scrollbar w-full md:w-64 md:border-r border-default',
className
)}
>
<div className="flex-1 flex flex-col">
{backToDashboardURL && (
<div className="flex-shrink-0 hidden md:block">
<div className="flex h-12 max-h-12 items-center border-b px-6 border-default">
<Link
href={backToDashboardURL}
className="flex text-sm flex-row gap-2 items-center text-foreground-lighter focus-visible:text-foreground hover:text-foreground"
>
<ArrowLeft strokeWidth={1.5} size={16} />
Back to dashboard
</Link>
</div>
</div>
)}
{header && header}
<div className="flex-1 overflow-auto">
<div className="flex flex-col space-y-8">
<Menu type="pills">
{customSidebarContent}
{sections.map((section, idx) => (
<div key={section.key || section.heading}>
{Boolean(section.heading) ? (
<SectionWithHeaders
key={section.key}
section={section}
subitems={subitems}
subitemsParentKey={subitemsParentKey}
/>
) : (
<div className="my-6 space-y-8">
<div className="mx-3">
{section.links.map((link, i: number) => {
const isActive = link.isActive && !subitems
return (
<Menu.Item
key={`${link.key}-${i}`}
rounded
active={isActive}
icon={link.icon}
>
<Link href={link.href || ''} className="block">
<div className="flex w-full items-center justify-between gap-1">
<div className="flex items-center gap-2 truncate w-full">
<span className="truncate">{link.label}</span>
</div>
</div>
</Link>
</Menu.Item>
)
})}
</div>
</div>
)}
{idx !== sections.length - 1 && <div className="h-px w-full bg-border-overlay" />}
</div>
))}
</Menu>
</div>
</div>
<footer className="p-2">
<NavUser />
</footer>
</div>
</>
</div>
)
}
@@ -2,7 +2,7 @@ import { useIsNavigationV2Enabled } from 'components/interfaces/App/FeaturePrevi
import { useRouter } from 'next/router'
import { PropsWithChildren } from 'react'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayoutV2'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayout'
import { ProjectLayout } from '../ProjectLayout'
import { AdvisorsSidebarMenu } from './AdvisorsSidebarMenu'
import { withAuth } from '@/hooks/misc/withAuth'
@@ -3,7 +3,7 @@ import { useIsNavigationV2Enabled } from 'components/interfaces/App/FeaturePrevi
import { useRouter } from 'next/router'
import type { PropsWithChildren } from 'react'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayoutV2'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayout'
import { ProjectLayout } from '../ProjectLayout'
import { useGenerateAuthMenu } from './AuthLayout.utils'
import { ProductMenu } from '@/components/ui/ProductMenu'
@@ -2,7 +2,7 @@ import { useIsNavigationV2Enabled } from 'components/interfaces/App/FeaturePrevi
import { useRouter } from 'next/router'
import type { PropsWithChildren } from 'react'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayoutV2'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayout'
import { ProjectLayout } from '../ProjectLayout'
import { useGenerateDatabaseMenu } from './DatabaseMenu.utils'
import { ProductMenu } from '@/components/ui/ProductMenu'
@@ -7,7 +7,7 @@ import { ResizablePanel, ResizablePanelGroup } from 'ui'
import { DefaultLayoutProviders } from './DefaultLayoutProviders'
import { LayoutHeader } from './Navigation/LayoutHeader/LayoutHeader'
import MobileNavigationBar from './Navigation/NavigationBar/MobileNavigationBar'
import { DefaultLayoutV2 } from './NavigationV2/DefaultLayoutV2'
import { DefaultLayoutV2 } from './NavigationV2/DefaultLayout'
import { LayoutSidebar } from './ProjectLayout/LayoutSidebar'
import { AppBannerWrapper } from '@/components/interfaces/App/AppBannerWrapper'
import { Sidebar } from '@/components/interfaces/Sidebar'
@@ -3,7 +3,7 @@ import { useIsNavigationV2Enabled } from 'components/interfaces/App/FeaturePrevi
import { useRouter } from 'next/router'
import type { ComponentProps, PropsWithChildren } from 'react'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayoutV2'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayout'
import { ProjectLayout } from '../ProjectLayout'
import { ProductMenu } from '@/components/ui/ProductMenu'
import { withAuth } from '@/hooks/misc/withAuth'
@@ -1,6 +1,6 @@
import { useIsNavigationV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations'
import { ProjectLayoutV2 } from 'components/layouts/NavigationV2/ProjectLayoutV2'
import { ProjectLayoutV2 } from 'components/layouts/NavigationV2/ProjectLayout'
import { ProjectLayout } from 'components/layouts/ProjectLayout'
import AlertError from 'components/ui/AlertError'
import { ProductMenu } from 'components/ui/ProductMenu'
@@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useIsNavigationV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import type { PropsWithChildren } from 'react'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayoutV2'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayout'
import { ProjectLayout } from '../ProjectLayout'
import { LogsSidebarMenuV2 } from './LogsSidebarMenuV2'
import NoPermission from '@/components/ui/NoPermission'
@@ -36,7 +36,7 @@ import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganizati
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { IS_PLATFORM } from '@/lib/constants'
const LayoutHeaderDivider = ({ className, ...props }: React.HTMLProps<HTMLSpanElement>) => (
export const LayoutHeaderDivider = ({ className, ...props }: React.HTMLProps<HTMLSpanElement>) => (
<span className={cn('text-border-stronger pr-2', className)} {...props}>
<svg
viewBox="0 0 24 24"
@@ -57,13 +57,10 @@ export function OrgSelector() {
const triggerButton = (
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground gap-2 h-auto text-left group px-1.5 py-1 touch-manipulation"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground gap-2 h-10 text-left group px-1.5 py-1 touch-manipulation"
onClick={isMobile ? () => setOpen(true) : undefined}
>
<span className="flex w-8 aspect-square shrink-0 items-center justify-center rounded border bg-surface-100 text-xs font-medium text-foreground-lighter">
{selectedOrgInitial}
</span>
<div className="flex min-w-0 flex-1 flex-col text-left -mb-0.5">
<div className="flex min-w-0 flex-1 flex-col text-left pl-0.5">
<div className="truncate text-foreground font-medium leading-tight min-w-[100px] max-w-[250px]">
{selectedOrganization?.name ?? 'Select organization'}
</div>
@@ -44,12 +44,11 @@ export function ProjectBranchSelector() {
)
const selectedBranch = branches?.find((b) => b.project_ref === ref)
const { isMainBranch, branchDisplayName, selectedOrgInitial, organizationHref } =
getProjectBranchSelectorState({
selectedBranch,
isBranchingEnabled,
selectedOrganization: selectedOrganization ?? undefined,
})
const { isMainBranch, branchDisplayName, organizationHref } = getProjectBranchSelectorState({
selectedBranch,
isBranchingEnabled,
selectedOrganization: selectedOrganization ?? undefined,
})
const goToOrganization = () => {
setOpen(false)
@@ -75,7 +74,7 @@ export function ProjectBranchSelector() {
const triggerProps = {
displayProjectName: displayProject.name,
selectedOrgInitial,
selectedOrg: selectedOrganization,
isBranch,
isProductionBranch,
branchDisplayName,
@@ -5,7 +5,6 @@ export interface ProjectBranchSelectorState {
selectedBranch: Branch | undefined
isMainBranch: boolean
branchDisplayName: string
selectedOrgInitial: string
organizationHref: string
}
@@ -18,7 +17,6 @@ export function getProjectBranchSelectorState(params: {
const isMainBranch = !isBranchingEnabled || selectedBranch?.is_default === true
const branchDisplayName = isBranchingEnabled ? (selectedBranch?.name ?? 'main') : 'main'
const selectedOrgInitial = selectedOrganization?.name?.trim().charAt(0).toUpperCase() ?? 'O'
const organizationHref = selectedOrganization?.slug
? `/org/${selectedOrganization.slug}`
: '/organizations'
@@ -27,7 +25,10 @@ export function getProjectBranchSelectorState(params: {
selectedBranch,
isMainBranch,
branchDisplayName,
selectedOrgInitial,
organizationHref,
}
}
export function getSelectedOrgInitial(orgName: string): string {
return orgName.trim().charAt(0).toUpperCase()
}
@@ -17,8 +17,6 @@ import { useEffect, useState } from 'react'
import { useAppStateSnapshot } from 'state/app-state'
import type { Organization } from 'types'
import {
Button,
cn,
Command_Shadcn_,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
@@ -113,6 +111,7 @@ export function ProjectBranchSelectorPopover({ onClose }: ProjectBranchSelectorP
selectedBranch={selectedBranch}
isBranchingEnabled={isBranchingEnabled}
isBranchesLoaded={isBranchesSuccess}
projectRef={parentRef ?? ref ?? ''}
onSelect={(branch) => {
const sanitizedRoute = sanitizeRoute(router.route, router.query)
const href =
@@ -238,6 +237,7 @@ function BranchColumn({
selectedBranch,
isBranchingEnabled,
isBranchesLoaded,
projectRef,
onSelect,
onCreateBranch,
onManageBranches,
@@ -247,6 +247,7 @@ function BranchColumn({
selectedBranch?: Branch
isBranchingEnabled: boolean
isBranchesLoaded: boolean
projectRef: string
onSelect: (branch: Branch) => void
onCreateBranch: () => void
onManageBranches: () => void
@@ -301,6 +302,7 @@ function BranchColumn({
</>
)}
</CommandList_Shadcn_>
<CommandSeparator_Shadcn_ />
<CommandGroup_Shadcn_ className="space-y-0.5">
<CommandItem_Shadcn_ className="cursor-pointer w-full" onSelect={() => onClose()}>
<Link
@@ -1,12 +1,19 @@
import { useIsNavigationV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ChevronsUpDown, GitBranch } from 'lucide-react'
import * as React from 'react'
import { forwardRef } from 'react'
import { cn, SidebarMenuButton as SidebarMenuButtonComponent } from 'ui'
import {
cn,
SidebarMenuButton as SidebarMenuButtonComponent,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { getSelectedOrgInitial } from './ProjectBranchSelector.utils'
import type { Organization } from '@/types'
export interface ProjectBranchSelectorTriggerProps {
displayProjectName: string
selectedOrgInitial: string
selectedOrg: Organization
isBranch: boolean
isProductionBranch: boolean
branchDisplayName: string
@@ -20,46 +27,46 @@ export const ProjectBranchSelectorTrigger = React.forwardRef<
React.ComponentPropsWithoutRef<typeof SidebarMenuButtonComponent>,
keyof ProjectBranchSelectorTriggerProps
>
>(
(
{ displayProjectName, selectedOrgInitial, isBranch, branchDisplayName, ...buttonProps },
ref
) => {
const isNavigationV2 = useIsNavigationV2Enabled()
>(({ displayProjectName, selectedOrg, isBranch, branchDisplayName, ...buttonProps }, ref) => {
const selectedOrgInitial = getSelectedOrgInitial(selectedOrg?.name ?? 'O')
return (
<SidebarMenuButtonComponent
ref={ref}
size="lg"
className="group py-1 gap-1.5 w-full flex h-auto text-left data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground touch-manipulation"
{...buttonProps}
>
<div className="relative flex h-8 aspect-square shrink-0 items-center bg-background-muted group-hover:border-stronger justify-center rounded border border-strong text-xs">
{selectedOrgInitial}
</div>
return (
<SidebarMenuButtonComponent
ref={ref}
size="lg"
className="group py-1 gap-1.5 w-full flex h-auto text-left data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground touch-manipulation"
{...buttonProps}
>
<Tooltip>
<TooltipTrigger>
<div className="relative flex h-8 aspect-square shrink-0 items-center bg-background-muted group-hover:border-stronger justify-center rounded border border-strong text-xs">
{selectedOrgInitial}
</div>
</TooltipTrigger>
<TooltipContent>{selectedOrg?.name}</TooltipContent>
</Tooltip>
<div className="text-left flex-grow min-w-0">
<div className="text-left flex-grow min-w-0">
<div className="text-left flex-grow min-w-0">
<div className="w-full truncate text-foreground leading-tight max-w-[250px]">
{displayProjectName}
</div>
<div
className={cn(
'flex items-center gap-0.5',
isBranch ? 'text-foreground-lighter' : 'text-warning'
)}
>
<GitBranch className="shrink-0 size-3" strokeWidth={1.5} />
<span className="truncate min-w-0 leading-tight text-xs">{branchDisplayName}</span>
</div>
<div className="w-full truncate text-foreground leading-tight max-w-[250px]">
{displayProjectName}
</div>
<div
className={cn(
'flex items-center gap-0.5',
isBranch ? 'text-foreground-lighter' : 'text-warning'
)}
>
<GitBranch className="shrink-0 size-3" strokeWidth={1.5} />
<span className="truncate min-w-0 leading-tight text-xs">{branchDisplayName}</span>
</div>
</div>
</div>
<ChevronsUpDown
strokeWidth={1.5}
className="ml-auto text-foreground-lighter !w-4 !h-4 md:hidden md:group-hover:flex"
/>
</SidebarMenuButtonComponent>
)
}
)
<ChevronsUpDown
strokeWidth={1.5}
className="ml-auto text-foreground-lighter !w-4 !h-4 md:hidden md:group-hover:flex"
/>
</SidebarMenuButtonComponent>
)
})
ProjectBranchSelectorTrigger.displayName = 'ProjectBranchSelectorTrigger'
@@ -1,4 +1,3 @@
import { usePHFlag } from 'hooks/ui/useFlag'
import { IS_PLATFORM } from 'lib/constants'
import { Plug, Search } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
@@ -11,6 +10,7 @@ import { NavGroup } from './NavGroup'
import { NavUser } from './NavUser'
import { useAppSidebarNavItems, type AppSidebarScope } from './useAppSidebarNavItems'
import { ConnectSheet } from '@/components/interfaces/ConnectSheet/ConnectSheet'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
export type { AppSidebarScope }
@@ -21,9 +21,7 @@ interface AppSidebarV2Props {
export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
const setCommandMenuOpen = useSetCommandMenuOpen()
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const connectSheetFlag = usePHFlag<string | boolean>('connectSheet')
const isFlagResolved = connectSheetFlag !== undefined
const isConnectSheetEnabled = connectSheetFlag === true || connectSheetFlag === 'variation'
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const {
isProjectScope,
@@ -78,14 +76,14 @@ export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
<NavGroup id="observability" label="Observability" items={observabilityItems} />
<NavGroup id="integrations" label="Integrations" items={integrationsItems} />
</>
) : (
) : selectedOrganization ? (
<NavGroup
id="organization"
label="Organization"
items={organizationItems}
isCollapsible={false}
/>
)}
) : null}
</SidebarContent>
<div
aria-hidden
@@ -3,12 +3,12 @@ import { AppBannerWrapper } from 'components/interfaces/App/AppBannerWrapper'
import MobileNavigationBar from 'components/layouts/Navigation/NavigationBar/MobileNavigationBar'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useRouter } from 'next/router'
import { PropsWithChildren } from 'react'
import type { PropsWithChildren } from 'react'
import { useAppStateSnapshot } from 'state/app-state'
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
import { DefaultLayoutProviders } from '../DefaultLayoutProviders'
import { AppSidebarV2 } from './AppSidebarV2'
import { AppSidebarV2 } from './AppSidebar'
import { RightRailLayout } from './RightIconRail'
export interface DefaultLayoutV2Props {
@@ -1,12 +1,8 @@
import { useFeaturePreviewModal } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { UserDropdownContent } from 'components/interfaces/UserDropdown'
import { ProfileImage } from 'components/ui/ProfileImage'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useProfileNameAndPicture } from 'lib/profile'
import { ChevronsUpDown, Loader2 } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/router'
import { useAppStateSnapshot } from 'state/app-state'
import {
DropdownMenu,
DropdownMenuContent,
@@ -0,0 +1,96 @@
import { mergeRefs } from 'common'
import { CreateBranchModal } from 'components/interfaces/BranchManagement/CreateBranchModal'
import { ProjectAPIDocs } from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs'
import { ResourceExhaustionWarningBanner } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
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 Head from 'next/head'
import { useRouter } from 'next/router'
import { forwardRef, type PropsWithChildren } from 'react'
import { useSetMainScrollContainer } from '../MainScrollContainerContext'
import { ContentWrapper } from '../ProjectLayout'
import { ProjectPausedState } from '../ProjectLayout/PausedState/ProjectPausedState'
export interface ProjectLayoutV2Props {
title?: string
isLoading?: boolean
isBlocking?: boolean
product?: string
selectedTable?: string
}
/**
* V2 Project layout - renders content directly without a secondary product menu sidebar.
* In V2, all navigation is in the primary sidebar (AppSidebar), so this layout
* only handles title, loading states, and content wrapping.
*/
export const ProjectLayoutV2 = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayoutV2Props>>(
({ title, isLoading = false, isBlocking = true, product = '', children, selectedTable }, ref) => {
const router = useRouter()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { data: selectedProject } = useSelectedProjectQuery()
const setMainScrollContainer = useSetMainScrollContainer()
const combinedRef = mergeRefs(ref, setMainScrollContainer)
const { appTitle } = useCustomContent(['app:title'])
const titleSuffix = appTitle || 'Supabase'
const projectName = selectedProject?.name
const organizationName = selectedOrganization?.name
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')
const showPausedState = isPaused && !ignorePausedState
return (
<>
<Head>
<title>
{title
? `${title} | ${titleSuffix}`
: selectedTable
? `${selectedTable} | ${projectName} | ${organizationName} | ${titleSuffix}`
: projectName
? `${projectName} | ${organizationName} | ${titleSuffix}`
: organizationName
? `${organizationName} | ${titleSuffix}`
: titleSuffix}
</title>
<meta name="description" content="Supabase Studio" />
</Head>
<main
className="@container flex h-full min-h-0 w-full flex-1 flex-col overflow-x-hidden overflow-y-auto"
ref={combinedRef}
>
{showPausedState ? (
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
<div className="w-full">
<ProjectPausedState product={product} />
</div>
</div>
) : (
<ContentWrapper isLoading={isLoading} isBlocking={isBlocking}>
<ResourceExhaustionWarningBanner />
{children}
</ContentWrapper>
)}
</main>
<CreateBranchModal />
<ProjectAPIDocs />
</>
)
}
)
ProjectLayoutV2.displayName = 'ProjectLayoutV2'
export const ProjectLayoutV2WithAuth = withAuth(ProjectLayoutV2)
@@ -1,235 +0,0 @@
import { mergeRefs, useParams } from 'common'
import { CreateBranchModal } from 'components/interfaces/BranchManagement/CreateBranchModal'
import { ProjectAPIDocs } from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs'
import { ResourceExhaustionWarningBanner } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { withAuth } from 'hooks/misc/withAuth'
import { usePHFlag } from 'hooks/ui/useFlag'
import { PROJECT_STATUS } from 'lib/constants'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { forwardRef, Fragment, PropsWithChildren, useEffect } from 'react'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { LogoLoader } from 'ui'
import { useSetMainScrollContainer } from '../MainScrollContainerContext'
import BuildingState from '../ProjectLayout/BuildingState'
import ConnectingState from '../ProjectLayout/ConnectingState'
import { LoadingState } from '../ProjectLayout/LoadingState'
import { ProjectPausedState } from '../ProjectLayout/PausedState/ProjectPausedState'
import { PauseFailedState } from '../ProjectLayout/PauseFailedState'
import { PausingState } from '../ProjectLayout/PausingState'
import { ResizingState } from '../ProjectLayout/ResizingState'
import RestartingState from '../ProjectLayout/RestartingState'
import { RestoreFailedState } from '../ProjectLayout/RestoreFailedState'
import { RestoringState } from '../ProjectLayout/RestoringState'
import { UpgradingState } from '../ProjectLayout/UpgradingState'
const routesToIgnoreProjectDetailsRequest = [
'/project/[ref]/settings/general',
'/project/[ref]/database/settings',
'/project/[ref]/storage/settings',
'/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',
'/project/[ref]/functions',
]
const routesToIgnorePostgrestConnection = [
'/project/[ref]/reports',
'/project/[ref]/settings/general',
'/project/[ref]/database/settings',
'/project/[ref]/settings/infrastructure',
'/project/[ref]/settings/addons',
]
export interface ProjectLayoutV2Props {
title?: string
isLoading?: boolean
isBlocking?: boolean
product?: string
selectedTable?: string
}
/**
* V2 Project layout - renders content directly without a secondary product menu sidebar.
* In V2, all navigation is in the primary sidebar (AppSidebarV2), so this layout
* only handles title, loading states, and content wrapping.
*/
export const ProjectLayoutV2 = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayoutV2Props>>(
({ title, isLoading = false, isBlocking = true, product = '', children, selectedTable }, ref) => {
const router = useRouter()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { data: selectedProject } = useSelectedProjectQuery()
const setMainScrollContainer = useSetMainScrollContainer()
const combinedRef = mergeRefs(ref, setMainScrollContainer)
const { appTitle } = useCustomContent(['app:title'])
const titleSuffix = appTitle || 'Supabase'
const projectName = selectedProject?.name
const organizationName = selectedOrganization?.name
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')
const showPausedState = isPaused && !ignorePausedState
return (
<>
<Head>
<title>
{title
? `${title} | ${titleSuffix}`
: selectedTable
? `${selectedTable} | ${projectName} | ${organizationName} | ${titleSuffix}`
: projectName
? `${projectName} | ${organizationName} | ${titleSuffix}`
: organizationName
? `${organizationName} | ${titleSuffix}`
: titleSuffix}
</title>
<meta name="description" content="Supabase Studio" />
</Head>
<main
className="@container flex h-full min-h-0 w-full flex-1 flex-col overflow-x-hidden overflow-y-auto"
ref={combinedRef}
>
{showPausedState ? (
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
<div className="w-full">
<ProjectPausedState product={product} />
</div>
</div>
) : (
<ContentWrapper isLoading={isLoading} isBlocking={isBlocking}>
<ResourceExhaustionWarningBanner />
{children}
</ContentWrapper>
)}
</main>
<CreateBranchModal />
<ProjectAPIDocs />
</>
)
}
)
ProjectLayoutV2.displayName = 'ProjectLayoutV2'
export const ProjectLayoutV2WithAuth = withAuth(ProjectLayoutV2)
interface ContentWrapperProps {
isLoading: boolean
isBlocking?: boolean
children: React.ReactNode
}
const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapperProps) => {
const router = useRouter()
const { ref } = useParams()
const state = useDatabaseSelectorStateSnapshot()
const { data: selectedProject } = useSelectedProjectQuery()
const isHomeNew = usePHFlag('homeNew') === 'new-home'
const isBranchesPage = router.pathname.includes('/project/[ref]/branches')
const isSettingsPages = router.pathname.includes('/project/[ref]/settings')
const isEdgeFunctionPages = router.pathname.includes('/project/[ref]/functions')
const isVaultPage = router.pathname === '/project/[ref]/settings/vault'
const isBackupsPage = router.pathname.includes('/project/[ref]/database/backups')
const isHomePage = router.pathname === '/project/[ref]'
const requiresDbConnection: boolean =
(!isEdgeFunctionPages &&
!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.PAUSING
const isProjectPauseFailed = selectedProject?.status === PROJECT_STATUS.PAUSE_FAILED
const isProjectOffline = selectedProject?.postgrestStatus === 'OFFLINE'
const shouldRedirectToHomeForBuilding =
isHomeNew && requiresDbConnection && isProjectBuilding && !isBranchesPage && !isHomePage
const shouldShowBuildingState =
requiresDbConnection && isProjectBuilding && !isBranchesPage && !(isHomeNew && 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 (requiresPostgrestConnection && isProjectOffline) {
return <ConnectingState project={selectedProject} />
}
if (requiresDbConnection && isProjectRestoring) {
return <RestoringState />
}
if (isProjectRestoreFailed && !isBackupsPage && !isEdgeFunctionPages) {
return <RestoreFailedState />
}
if (shouldRedirectToHomeForBuilding) {
return <LogoLoader />
}
if (shouldShowBuildingState) {
return <BuildingState />
}
return <Fragment key={selectedProject?.ref}>{children}</Fragment>
}
@@ -1,6 +1,6 @@
export { DefaultLayoutV2 } from './DefaultLayoutV2'
export { ProjectLayoutV2, ProjectLayoutV2WithAuth } from './ProjectLayoutV2'
export { AppSidebarV2 } from './AppSidebarV2'
export { DefaultLayoutV2 } from './DefaultLayout'
export { ProjectLayoutV2, ProjectLayoutV2WithAuth } from './ProjectLayout'
export { AppSidebarV2 } from './AppSidebar'
export { NavGroup } from './NavGroup'
export type { NavGroupItem, NavGroupProps } from './NavGroup'
export { RightRailLayout } from './RightIconRail'
@@ -21,7 +21,7 @@ import {
generateDatabaseNavItems,
generateObservabilityNavItems,
generatePlatformNavItems,
} from './NavigationV2.utils'
} from './Navigation.utils'
export type AppSidebarScope = 'project' | 'organization'
@@ -3,7 +3,7 @@ import { useIsNavigationV2Enabled } from 'components/interfaces/App/FeaturePrevi
import { usePathname } from 'next/navigation'
import { useEffect, useRef, type PropsWithChildren } from 'react'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayoutV2'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayout'
import { ProjectLayout } from '../ProjectLayout'
import ObservabilityMenu from './ObservabilityMenu'
import { useIndexAdvisorStatus } from '@/components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus'
@@ -318,7 +318,7 @@ const MenuBarWrapper = ({
return !isLoading && productMenu && showMenuBar ? children : null
}
interface ContentWrapperProps {
export interface ContentWrapperProps {
isLoading: boolean
isBlocking?: boolean
children: ReactNode
@@ -336,7 +336,7 @@ interface ContentWrapperProps {
*
* [TODO] Next iteration should scrape long polling and just listen to the project's status
*/
const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapperProps) => {
export const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapperProps) => {
const router = useRouter()
const { ref } = useParams()
const state = useDatabaseSelectorStateSnapshot()
@@ -1,7 +1,7 @@
import { useRouter } from 'next/router'
import type { PropsWithChildren } from 'react'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayoutV2'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayout'
import { ProjectLayout } from '../ProjectLayout'
import { useGenerateSettingsMenu } from './SettingsMenu.utils'
import { useIsNavigationV2Enabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
@@ -2,7 +2,7 @@ import { useIsNavigationV2Enabled } from 'components/interfaces/App/FeaturePrevi
import { useRouter } from 'next/router'
import type { PropsWithChildren } from 'react'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayoutV2'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayout'
import { ProjectLayout } from '../ProjectLayout'
import { generateRealtimeMenu } from './RealtimeMenu.utils'
import { ProductMenu } from '@/components/ui/ProductMenu'
@@ -1,6 +1,6 @@
import type { ReactNode } from 'react'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayoutV2'
import { ProjectLayoutV2 } from '../NavigationV2/ProjectLayout'
import { ProjectLayout } from '../ProjectLayout'
import { useIsNavigationV2Enabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { StorageMenuV2 } from '@/components/interfaces/Storage/StorageMenuV2'
@@ -113,4 +113,4 @@ const ResizableHandle = ({
const useDefaultLayout = ResizablePrimitive.useDefaultLayout
const usePanelRef = ResizablePrimitive.usePanelRef
export { ResizableHandle, ResizablePanel, ResizablePanelGroup, type, useDefaultLayout, usePanelRef }
export { ResizableHandle, ResizablePanel, ResizablePanelGroup, useDefaultLayout, usePanelRef }