mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
nav v2
This commit is contained in:
13 files changed
+389
-238
No files matched your search
@@ -39,7 +39,7 @@ export function UserDropdown({
|
||||
<DropdownMenuTrigger asChild className={cn('border flex-shrink-0 px-3', triggerClassName)}>
|
||||
<Button
|
||||
type="default"
|
||||
className="[&>span]:flex px-0 py-0 rounded-full overflow-hidden h-8 w-8"
|
||||
className="[&>span]:flex px-0 py-0 rounded-full overflow-hidden w-8 h-8"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
|
||||
@@ -3,9 +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'
|
||||
|
||||
interface WithSidebarProps {
|
||||
title: string
|
||||
@@ -132,9 +130,6 @@ export const SidebarContent = ({
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<footer className="p-2">
|
||||
<NavUser />
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,8 +5,8 @@ import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { Badge, cn } from 'ui'
|
||||
import { ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
import { CommandMenuTriggerInput } from 'ui-patterns'
|
||||
|
||||
import { BreadcrumbsView } from './BreadcrumbsView'
|
||||
@@ -14,6 +14,7 @@ import { FeedbackDropdown } from './FeedbackDropdown/FeedbackDropdown'
|
||||
import { HomeIcon } from './HomeIcon'
|
||||
import { LocalVersionPopover } from './LocalVersionPopover'
|
||||
import { MergeRequestButton } from './MergeRequestButton'
|
||||
import OrgProjectBranchNav from './OrgProjectBranchNav'
|
||||
import {
|
||||
useIsBranching2Enabled,
|
||||
useIsFloatingMobileToolbarEnabled,
|
||||
@@ -24,15 +25,9 @@ import { LocalDropdown } from '@/components/interfaces/LocalDropdown'
|
||||
import { UserDropdown } from '@/components/interfaces/UserDropdown'
|
||||
import { AdvisorButton } from '@/components/layouts/AppLayout/AdvisorButton'
|
||||
import { AssistantButton } from '@/components/layouts/AppLayout/AssistantButton'
|
||||
import { BranchDropdown } from '@/components/layouts/AppLayout/BranchDropdown'
|
||||
import { InlineEditorButton } from '@/components/layouts/AppLayout/InlineEditorButton'
|
||||
import { OrganizationDropdown } from '@/components/layouts/AppLayout/OrganizationDropdown'
|
||||
import { ProjectDropdown } from '@/components/layouts/AppLayout/ProjectDropdown'
|
||||
import { HelpButton } from '@/components/ui/HelpPanel/HelpButton'
|
||||
import { getResourcesExceededLimitsOrg } from '@/components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
|
||||
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
|
||||
@@ -69,9 +64,8 @@ export const LayoutHeader = ({
|
||||
backToDashboardURL,
|
||||
}: LayoutHeaderProps) => {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef, slug } = useParams()
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: selectedProject } = useSelectedProjectQuery()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
const gitlessBranching = useIsBranching2Enabled()
|
||||
|
||||
const showFloatingMobileToolbar = useIsFloatingMobileToolbarEnabled()
|
||||
@@ -79,29 +73,12 @@ export const LayoutHeader = ({
|
||||
|
||||
const isAccountPage = router.pathname.startsWith('/account')
|
||||
|
||||
// We only want to query the org usage and check for possible over-ages for plans without usage billing enabled (free or pro with spend cap)
|
||||
const { data: orgUsage } = useOrgUsageQuery(
|
||||
{ orgSlug: selectedOrganization?.slug },
|
||||
{ enabled: selectedOrganization?.usage_billing_enabled === false }
|
||||
)
|
||||
|
||||
const exceedingLimits = useMemo(() => {
|
||||
if (orgUsage) {
|
||||
return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, [orgUsage])
|
||||
|
||||
const isNewProject =
|
||||
selectedProject?.inserted_at !== undefined &&
|
||||
dayjs(selectedProject.inserted_at).isAfter(dayjs().subtract(5, 'day'))
|
||||
|
||||
const connectButtonType = isNewProject ? 'primary' : 'default'
|
||||
|
||||
// show org selection if we are on a project page or on a explicit org route
|
||||
const showOrgSelection = slug || (selectedOrganization && projectRef)
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
@@ -146,46 +123,7 @@ export const LayoutHeader = ({
|
||||
</div>
|
||||
<div className="hidden md:flex items-center text-sm">
|
||||
<HomeIcon />
|
||||
<div className="flex items-center md:pl-2">
|
||||
{showOrgSelection && IS_PLATFORM ? (
|
||||
<>
|
||||
<LayoutHeaderDivider className="hidden md:block" />
|
||||
<OrganizationDropdown />
|
||||
</>
|
||||
) : null}
|
||||
<AnimatePresence>
|
||||
{projectRef && (
|
||||
<motion.div
|
||||
className="flex items-center"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{
|
||||
duration: 0.15,
|
||||
ease: 'easeOut',
|
||||
}}
|
||||
>
|
||||
{IS_PLATFORM && <LayoutHeaderDivider />}
|
||||
<ProjectDropdown />
|
||||
|
||||
{exceedingLimits && (
|
||||
<div className="ml-2">
|
||||
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
|
||||
<Badge variant="destructive">Exceeding usage limits</Badge>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProject && IS_PLATFORM && (
|
||||
<>
|
||||
<LayoutHeaderDivider />
|
||||
<BranchDropdown />
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<OrgProjectBranchNav className="ml-2" />
|
||||
<AnimatePresence>
|
||||
{headerTitle && (
|
||||
<motion.div
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { IS_PLATFORM, useParams } from 'common'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { Link } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { Badge, cn } from 'ui'
|
||||
|
||||
import { LayoutHeaderDivider } from './LayoutHeader'
|
||||
import { BranchDropdown } from '@/components/layouts/AppLayout/BranchDropdown'
|
||||
import { OrganizationDropdown } from '@/components/layouts/AppLayout/OrganizationDropdown'
|
||||
import { ProjectDropdown } from '@/components/layouts/AppLayout/ProjectDropdown'
|
||||
import { getResourcesExceededLimitsOrg } from '@/components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
|
||||
const OrgProjectBranchNav = ({ className }: { className?: string }) => {
|
||||
const { ref: projectRef, slug } = useParams()
|
||||
const { data: selectedProject } = useSelectedProjectQuery()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
|
||||
// We only want to query the org usage and check for possible over-ages for plans without usage billing enabled (free or pro with spend cap)
|
||||
const { data: orgUsage } = useOrgUsageQuery(
|
||||
{ orgSlug: selectedOrganization?.slug },
|
||||
{ enabled: selectedOrganization?.usage_billing_enabled === false }
|
||||
)
|
||||
|
||||
const exceedingLimits = useMemo(() => {
|
||||
if (orgUsage) {
|
||||
return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, [orgUsage])
|
||||
|
||||
// show org selection if we are on a project page or on a explicit org route
|
||||
const showOrgSelection = slug || (selectedOrganization && projectRef)
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{showOrgSelection && IS_PLATFORM ? (
|
||||
<>
|
||||
<LayoutHeaderDivider />
|
||||
<OrganizationDropdown />
|
||||
</>
|
||||
) : null}
|
||||
<AnimatePresence>
|
||||
{projectRef && (
|
||||
<motion.div
|
||||
className="flex items-center"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{
|
||||
duration: 0.15,
|
||||
ease: 'easeOut',
|
||||
}}
|
||||
>
|
||||
{IS_PLATFORM && <LayoutHeaderDivider />}
|
||||
<ProjectDropdown />
|
||||
|
||||
{exceedingLimits && (
|
||||
<div className="ml-2">
|
||||
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
|
||||
<Badge variant="destructive">Exceeding usage limits</Badge>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProject && IS_PLATFORM && (
|
||||
<>
|
||||
<LayoutHeaderDivider />
|
||||
<BranchDropdown />
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgProjectBranchNav
|
||||
+1
-1
@@ -13,7 +13,7 @@ import type { Organization } from '@/types'
|
||||
|
||||
export interface ProjectBranchSelectorTriggerProps {
|
||||
displayProjectName: string
|
||||
selectedOrg: Organization
|
||||
selectedOrg: Organization | undefined
|
||||
isBranch: boolean
|
||||
isProductionBranch: boolean
|
||||
branchDisplayName: string
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { Plug, Search } from 'lucide-react'
|
||||
import { Plug } from 'lucide-react'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { Button, Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from 'ui'
|
||||
import { useSetCommandMenuOpen } from 'ui-patterns'
|
||||
import { Button, Sidebar, SidebarContent, SidebarHeader } from 'ui'
|
||||
|
||||
import { OrgSelector } from '../Navigation/NavigationBar/OrgSelector'
|
||||
import { ProjectBranchSelector } from '../Navigation/NavigationBar/ProjectBranchSelector'
|
||||
import { NavGroup } from './NavGroup'
|
||||
import { NavUser } from './NavUser'
|
||||
import { useAppSidebarNavItems, type AppSidebarScope } from './useAppSidebarNavItems'
|
||||
import { ConnectSheet } from '@/components/interfaces/ConnectSheet/ConnectSheet'
|
||||
import { OrgSelector } from '@/components/layouts/Navigation/NavigationBar/OrgSelector'
|
||||
import { ProjectBranchSelector } from '@/components/layouts/Navigation/NavigationBar/ProjectBranchSelector'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
|
||||
export type { AppSidebarScope }
|
||||
|
||||
@@ -19,7 +17,6 @@ interface AppSidebarV2Props {
|
||||
}
|
||||
|
||||
export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
|
||||
const setCommandMenuOpen = useSetCommandMenuOpen()
|
||||
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
|
||||
@@ -34,40 +31,38 @@ export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
|
||||
organizationItems,
|
||||
} = useAppSidebarNavItems({ scope })
|
||||
|
||||
const showSelector = IS_PLATFORM && (isProjectScope || selectedOrganization)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar
|
||||
collapsible="none"
|
||||
className="hidden md:flex h-full w-full border-r border-default group"
|
||||
>
|
||||
<SidebarHeader className="gap-2 pt-2">
|
||||
<SidebarHeader className="gap-2">
|
||||
<div className="space-y-2">
|
||||
{isProjectScope ? <ProjectBranchSelector /> : IS_PLATFORM ? <OrgSelector /> : null}
|
||||
{isProjectScope && (
|
||||
<div className="flex items-center gap-2 px-1.5">
|
||||
<Button
|
||||
type="outline"
|
||||
size="small"
|
||||
onClick={() => setCommandMenuOpen(true)}
|
||||
className="h-8 min-w-8 !w-8 px-0 justify-center text-foreground-lighter font-normal bg-transparent"
|
||||
icon={<Search strokeWidth={1.5} />}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
disabled={!isActiveHealthy}
|
||||
onClick={() => setShowConnect(true)}
|
||||
className="h-8 flex-1 justify-center gap-0 pl-2"
|
||||
icon={<Plug className="rotate-90" strokeWidth={1.5} />}
|
||||
>
|
||||
<span>Connect</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{showSelector &&
|
||||
(isProjectScope ? (
|
||||
<ProjectBranchSelector />
|
||||
) : (
|
||||
selectedOrganization && <OrgSelector />
|
||||
))}
|
||||
<div className="flex items-center px-0.5">
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
disabled={!isActiveHealthy}
|
||||
onClick={() => setShowConnect(true)}
|
||||
className="h-7 flex-1 justify-center gap-0 pl-2"
|
||||
icon={<Plug className="rotate-90" strokeWidth={1.5} />}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
<div className="relative min-h-0 flex-1 overflow-hidden">
|
||||
<SidebarContent className="h-full gap-0">
|
||||
<SidebarContent className="h-full gap-0 pb-8">
|
||||
{isProjectScope ? (
|
||||
<>
|
||||
<NavGroup id="project" label="Project" items={projectItems} />
|
||||
@@ -75,26 +70,25 @@ export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
|
||||
<NavGroup id="platform" label="Platform" items={platformItems} />
|
||||
<NavGroup id="observability" label="Observability" items={observabilityItems} />
|
||||
<NavGroup id="integrations" label="Integrations" items={integrationsItems} />
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 top-0 z-10 h-3 bg-gradient-to-b from-sidebar to-transparent"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-3 bg-gradient-to-t from-sidebar to-transparent"
|
||||
/>
|
||||
</>
|
||||
) : selectedOrganization ? (
|
||||
) : (
|
||||
<NavGroup
|
||||
id="organization"
|
||||
label="Organization"
|
||||
label=""
|
||||
items={organizationItems}
|
||||
isCollapsible={false}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
</SidebarContent>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 top-0 z-10 h-4 bg-gradient-to-b from-sidebar to-transparent"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-4 bg-gradient-to-t from-sidebar to-transparent"
|
||||
/>
|
||||
</div>
|
||||
<SidebarFooter>{IS_PLATFORM && <NavUser />}</SidebarFooter>
|
||||
</Sidebar>
|
||||
|
||||
<ConnectSheet />
|
||||
|
||||
@@ -9,38 +9,37 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
|
||||
|
||||
import { DefaultLayoutProviders } from '../DefaultLayoutProviders'
|
||||
import { AppSidebarV2 } from './AppSidebar'
|
||||
import { RightRailLayout } from './RightIconRail'
|
||||
import { LayoutHeader } from './LayoutHeader'
|
||||
import { RightPanelToolbarLayout } from './RightPanelToolbar'
|
||||
|
||||
export interface DefaultLayoutV2Props {
|
||||
headerTitle?: string
|
||||
hideMobileMenu?: boolean
|
||||
}
|
||||
|
||||
const leftSidebarMinSize = 180
|
||||
const leftSidebarMinSize = 160
|
||||
const leftSidebarMaxSize = 450
|
||||
const leftSidebarDefaultSize = '250px'
|
||||
const leftSidebarDefaultSize = '230px'
|
||||
|
||||
const mainContentClass = 'flex h-full min-h-0 min-w-0 flex-col overflow-hidden'
|
||||
|
||||
/**
|
||||
* New three-column layout for the dashboard (V2 navigation).
|
||||
*
|
||||
* Layout structure:
|
||||
* 1. Left sidebar - navigation with groups (Database, Platform, Observability, Integrations)
|
||||
* 2. Main content area - page content (no secondary nav bar)
|
||||
* 3. Right icon rail - AI, SQL, Alerts, Help panels
|
||||
*
|
||||
* This replaces DefaultLayout + ProjectLayout + feature-specific layouts (AuthLayout, DatabaseLayout, etc.)
|
||||
* when the navigation V2 feature flag is enabled. The key difference is that there is no longer a
|
||||
* secondary product menu sidebar - all navigation is handled in the primary sidebar with collapsible groups.
|
||||
* V2 navigation shell: mobile bar, then AppSidebar (when shown) + main column. Main column has
|
||||
* LayoutHeader above the row of page content and right panel toolbar.
|
||||
*/
|
||||
export const DefaultLayoutV2 = ({
|
||||
children,
|
||||
headerTitle,
|
||||
hideMobileMenu,
|
||||
}: PropsWithChildren<DefaultLayoutV2Props>) => {
|
||||
const router = useRouter()
|
||||
const isMobile = useBreakpoint('md')
|
||||
const appSnap = useAppStateSnapshot()
|
||||
const scope = router.pathname.startsWith('/project') ? 'project' : 'organization'
|
||||
const showLeftSidebar = !isMobile && !router.pathname.startsWith('/account')
|
||||
const showLeftSidebar =
|
||||
!isMobile &&
|
||||
!router.pathname.startsWith('/account') &&
|
||||
!router.pathname.startsWith('/organizations')
|
||||
|
||||
const [lastVisitedOrganization] = useLocalStorageQuery(
|
||||
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
|
||||
@@ -55,17 +54,30 @@ export const DefaultLayoutV2 = ({
|
||||
: '/organizations'
|
||||
: undefined
|
||||
|
||||
const header = (
|
||||
<LayoutHeader headerTitle={headerTitle} backToDashboardURL={backToDashboardURL} scope={scope} />
|
||||
)
|
||||
|
||||
const contentRow = <RightPanelToolbarLayout>{children}</RightPanelToolbarLayout>
|
||||
|
||||
const mainColumn = (
|
||||
<div className={mainContentClass}>
|
||||
<div className="shrink-0">{header}</div>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">{contentRow}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<DefaultLayoutProviders>
|
||||
<div className="flex flex-col h-screen w-screen">
|
||||
<div className="flex h-screen w-screen flex-col">
|
||||
<AppBannerWrapper />
|
||||
<div className="flex-shrink-0">
|
||||
<div className="shrink-0">
|
||||
<MobileNavigationBar
|
||||
hideMobileMenu={hideMobileMenu}
|
||||
backToDashboardURL={backToDashboardURL}
|
||||
/>
|
||||
</div>
|
||||
<RightRailLayout>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
|
||||
{showLeftSidebar ? (
|
||||
<ResizablePanelGroup
|
||||
orientation="horizontal"
|
||||
@@ -82,17 +94,14 @@ export const DefaultLayoutV2 = ({
|
||||
<AppSidebarV2 scope={scope} />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle className="hidden md:flex bg-background" />
|
||||
<ResizablePanel
|
||||
id="panel-v2-main-content"
|
||||
className="h-full min-h-0 min-w-0 overflow-hidden"
|
||||
>
|
||||
<div className="flex h-full min-h-0 flex-1 overflow-hidden">{children}</div>
|
||||
<ResizablePanel id="panel-v2-main-column" className={mainContentClass}>
|
||||
{mainColumn}
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
) : (
|
||||
<div className="flex h-full min-h-0 flex-1 overflow-hidden">{children}</div>
|
||||
mainColumn
|
||||
)}
|
||||
</RightRailLayout>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayoutProviders>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { LOCAL_STORAGE_KEYS } from 'common'
|
||||
import { DevToolbarTrigger } from 'dev-tools'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo, type ReactNode } from 'react'
|
||||
import { Badge, cn } from 'ui'
|
||||
import { CommandMenuTriggerInput } from 'ui-patterns'
|
||||
|
||||
import { useAppSidebarNavItems, type AppSidebarScope } from './useAppSidebarNavItems'
|
||||
import { useIsFloatingMobileToolbarEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { ConnectSheet } from '@/components/interfaces/ConnectSheet/ConnectSheet'
|
||||
import { LocalDropdown } from '@/components/interfaces/LocalDropdown'
|
||||
import { UserDropdown } from '@/components/interfaces/UserDropdown'
|
||||
import { BreadcrumbsView } from '@/components/layouts/Navigation/LayoutHeader/BreadcrumbsView'
|
||||
import { FeedbackDropdown } from '@/components/layouts/Navigation/LayoutHeader/FeedbackDropdown/FeedbackDropdown'
|
||||
import { LayoutHeaderDivider } from '@/components/layouts/Navigation/LayoutHeader/LayoutHeader'
|
||||
import { LocalVersionPopover } from '@/components/layouts/Navigation/LayoutHeader/LocalVersionPopover'
|
||||
import { getResourcesExceededLimitsOrg } from '@/components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
|
||||
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
|
||||
interface LayoutHeaderProps {
|
||||
customHeaderComponents?: ReactNode
|
||||
breadcrumbs?: unknown[]
|
||||
headerTitle?: string
|
||||
backToDashboardURL?: string
|
||||
scope?: AppSidebarScope
|
||||
}
|
||||
|
||||
export const LayoutHeader = ({
|
||||
customHeaderComponents,
|
||||
breadcrumbs = [],
|
||||
headerTitle,
|
||||
backToDashboardURL,
|
||||
scope,
|
||||
}: LayoutHeaderProps) => {
|
||||
const router = useRouter()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
|
||||
const showFloatingMobileToolbar = useIsFloatingMobileToolbarEnabled()
|
||||
const [commandMenuEnabled] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU, true)
|
||||
|
||||
const isAccountPage = router.pathname.startsWith('/account')
|
||||
|
||||
// We only want to query the org usage and check for possible over-ages for plans without usage billing enabled (free or pro with spend cap)
|
||||
const { data: orgUsage } = useOrgUsageQuery(
|
||||
{ orgSlug: selectedOrganization?.slug },
|
||||
{ enabled: selectedOrganization?.usage_billing_enabled === false }
|
||||
)
|
||||
|
||||
const { isProjectScope } = useAppSidebarNavItems({ scope })
|
||||
|
||||
const exceedingLimits = useMemo(() => {
|
||||
if (orgUsage) {
|
||||
return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, [orgUsage])
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={cn(
|
||||
'flex h-11 md:h-12 items-center flex-shrink-0 border-b',
|
||||
showFloatingMobileToolbar && 'hidden md:flex'
|
||||
)}
|
||||
>
|
||||
{backToDashboardURL && isAccountPage && (
|
||||
<div className="flex items-center justify-center border-r flex-0 md:hidden h-full aspect-square">
|
||||
<Link
|
||||
href={backToDashboardURL}
|
||||
className="flex items-center justify-center border-none !bg-transparent rounded-md min-w-[30px] w-[30px] h-[30px] text-foreground-lighter hover:text-foreground transition-colors"
|
||||
>
|
||||
<ChevronLeft strokeWidth={1.5} size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between h-full px-1 pl-3 flex-1 overflow-x-auto gap-x-4'
|
||||
)}
|
||||
>
|
||||
<div className="hidden md:flex items-center justify-start text-sm gap-x-2">
|
||||
{/* <HomeIcon />
|
||||
<OrgProjectBranchNav /> */}
|
||||
{/* {IS_PLATFORM && (
|
||||
<div>
|
||||
{isProjectScope ? (
|
||||
<ProjectBranchSelector />
|
||||
) : (
|
||||
selectedOrganization && <OrgSelector />
|
||||
)}
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<AnimatePresence>
|
||||
{headerTitle && (
|
||||
<motion.div
|
||||
className="flex items-center"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{
|
||||
duration: 0.15,
|
||||
ease: 'easeOut',
|
||||
}}
|
||||
>
|
||||
{/* <HomeIcon className="pr-2" /> */}
|
||||
<LayoutHeaderDivider />
|
||||
<span className="text-foreground">{headerTitle}</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<BreadcrumbsView defaultValue={breadcrumbs} />
|
||||
|
||||
{exceedingLimits && (
|
||||
<div className="ml-2">
|
||||
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
|
||||
<Badge variant="destructive">Exceeding usage limits</Badge>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* <div className="flex items-center justify-center gap-x-2">
|
||||
<CommandMenuTriggerInput
|
||||
showShortcut={commandMenuEnabled}
|
||||
placeholder="Search..."
|
||||
className={cn(
|
||||
'flex max-w-32 xl:max-w-40 rounded-full bg-transparent border-strong',
|
||||
'[&_.command-shortcut>div]:border-none',
|
||||
'[&_.command-shortcut>div]:pr-2',
|
||||
'[&_.command-shortcut>div]:bg-transparent',
|
||||
'[&_.command-shortcut>div]:text-foreground-lighter'
|
||||
)}
|
||||
/>
|
||||
</div> */}
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
{customHeaderComponents && customHeaderComponents}
|
||||
{IS_PLATFORM ? (
|
||||
<>
|
||||
<DevToolbarTrigger />
|
||||
<FeedbackDropdown />
|
||||
<CommandMenuTriggerInput
|
||||
showShortcut={commandMenuEnabled}
|
||||
placeholder="Search..."
|
||||
className={cn(
|
||||
'flex max-w-32 xl:max-w-12 w-auto rounded-full bg-transparent border-strong',
|
||||
'[&_.command-shortcut>div]:border-none',
|
||||
'[&_.command-shortcut>div]:pr-2',
|
||||
'[&_.command-shortcut>div]:bg-transparent',
|
||||
'[&_.command-shortcut>div]:text-foreground-lighter'
|
||||
)}
|
||||
/>
|
||||
<UserDropdown triggerClassName="hidden md:flex" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LocalVersionPopover />
|
||||
<CommandMenuTriggerInput
|
||||
showShortcut={commandMenuEnabled}
|
||||
placeholder="Search..."
|
||||
className={cn(
|
||||
'flex max-w-32 xl:max-w-32 rounded-full bg-transparent border-strong',
|
||||
'[&_.command-shortcut>div]:border-none',
|
||||
'[&_.command-shortcut>div]:pr-2',
|
||||
'[&_.command-shortcut>div]:bg-transparent',
|
||||
'[&_.command-shortcut>div]:text-foreground-lighter'
|
||||
)}
|
||||
/>
|
||||
<LocalDropdown triggerClassName="hidden md:flex" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ConnectSheet />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -65,14 +65,13 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
isActive={item.isActive}
|
||||
className="gap-2 text-foreground-light !py-1.5 !h-7"
|
||||
className="gap-2 text-foreground-lighter !h-6"
|
||||
>
|
||||
<NavItemIcon icon={item.icon} />
|
||||
<span className="truncate">{item.title}</span>
|
||||
{item.label && (
|
||||
<Badge className="ml-1 px-1.5 py-0.5 bg-transparent !border-stronger text-[10px] leading-none font-medium">
|
||||
<Badge className="ml-1 px-1.5 py-0.5 bg-transparent !border-stronger text-[10px] leading-none">
|
||||
{item.label}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -100,16 +99,15 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro
|
||||
) : (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
isActive={item.isActive}
|
||||
asChild
|
||||
className="gap-2 text-foreground-light !py-1.5 !h-7"
|
||||
className="gap-2 text-foreground-lighter !h-6"
|
||||
>
|
||||
<Link href={item.url}>
|
||||
<NavItemIcon icon={item.icon} />
|
||||
<span>{item.title}</span>
|
||||
{item.label && (
|
||||
<Badge className="ml-1 px-1.5 py-0.5 bg-transparent !border-stronger text-[10px] leading-none font-medium">
|
||||
<Badge className="ml-1 px-1.5 py-0.5 bg-transparent !border-stronger text-[10px] leading-none">
|
||||
{item.label}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -131,7 +129,7 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro
|
||||
<SidebarGroup>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarGroupLabel
|
||||
className={`group/label text-foreground-lighter flex items-center gap-1 ${
|
||||
className={`group/label text-foreground-lighter/70 flex items-center gap-1 !h-6 ${
|
||||
isCollapsible ? 'cursor-pointer hover:text-foreground-light' : 'pointer-events-none'
|
||||
}`}
|
||||
>
|
||||
@@ -139,7 +137,7 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro
|
||||
{isCollapsible && (
|
||||
<ChevronRight
|
||||
strokeWidth={1.5}
|
||||
className="!w-4 !h-4 text-foreground-muted group-hover/label:text-foreground-light transition-transform duration-200 group-data-[state=open]/group-collapsible:rotate-90 hidden group-hover:block"
|
||||
className="!w-4 !h-4 text-foreground-muted group-hover/label:text-foreground-lighter transition-transform duration-200 group-data-[state=open]/group-collapsible:rotate-90 hidden group-hover:block"
|
||||
/>
|
||||
)}
|
||||
</SidebarGroupLabel>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from 'ui'
|
||||
|
||||
export function NavUser() {
|
||||
const { isMobile } = useSidebar()
|
||||
const profileShowEmailEnabled = useIsFeatureEnabled('profile:show_email')
|
||||
const { username, primaryEmail, avatarUrl, isLoading } = useProfileNameAndPicture()
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex aspect-square size-8 items-center justify-center">
|
||||
<Loader2 className="animate-spin text-foreground-lighter" size={16} />
|
||||
</div>
|
||||
) : (
|
||||
<ProfileImage alt={username} src={avatarUrl} className="size-8 rounded-full" />
|
||||
)}
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{username}</span>
|
||||
{primaryEmail !== username && profileShowEmailEnabled && (
|
||||
<span className="truncate text-xs text-foreground-light">{primaryEmail}</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronsUpDown
|
||||
strokeWidth={1}
|
||||
className="ml-auto text-foreground-light hidden group-hover:block !w-4 !h-4"
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<UserDropdownContent />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
+34
-24
@@ -18,13 +18,13 @@ import { useMobileSheet } from '../Navigation/NavigationBar/MobileSheetContext'
|
||||
import { isSidebarId } from '../ProjectLayout/LayoutSidebar'
|
||||
import { SIDEBAR_KEYS } from '../ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
|
||||
|
||||
interface RailItem {
|
||||
interface ToolbarItem {
|
||||
id: string
|
||||
label: string
|
||||
icon: (props: { isActive: boolean }) => ReactNode
|
||||
}
|
||||
|
||||
const RAIL_ITEMS: RailItem[] = [
|
||||
const TOOLBAR_ITEMS: ToolbarItem[] = [
|
||||
{
|
||||
id: SIDEBAR_KEYS.AI_ASSISTANT,
|
||||
label: 'Assistant',
|
||||
@@ -47,7 +47,6 @@ const RAIL_ITEMS: RailItem[] = [
|
||||
icon: () => <Lightbulb size={16} strokeWidth={1.5} />,
|
||||
},
|
||||
{
|
||||
// id: SIDEBAR_KEYS.SUPPORT_PANEL,
|
||||
id: SIDEBAR_KEYS.HELP_PANEL,
|
||||
label: 'Help & Support',
|
||||
icon: () => <HelpCircle size={16} strokeWidth={1.5} />,
|
||||
@@ -58,7 +57,7 @@ const RIGHT_SIDEBAR_MIN_SIZE_PERCENTAGE = 300
|
||||
const RIGHT_SIDEBAR_DEFAULT_SIZE_PERCENTAGE = 340
|
||||
const RIGHT_SIDEBAR_MAX_SIZE_PERCENTAGE = 750
|
||||
|
||||
function RightIconRail() {
|
||||
function RightPanelToolbar() {
|
||||
const { activeSidebar, toggleSidebar } = useSidebarManagerSnapshot()
|
||||
|
||||
const isMobile = useBreakpoint('md')
|
||||
@@ -78,15 +77,14 @@ function RightIconRail() {
|
||||
|
||||
return (
|
||||
<aside className="bg-dash-sidebar text-foreground-lighter border-default flex w-10 shrink-0 border-l">
|
||||
<nav className="flex flex-1 flex-col items-center justify-center gap-2 py-2 pt-3">
|
||||
{RAIL_ITEMS.map((item) => {
|
||||
<nav className="flex flex-1 flex-col items-center justify-start gap-1 p-1">
|
||||
{TOOLBAR_ITEMS.map((item) => {
|
||||
const isActive = activeSidebar?.id === item.id
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<Tooltip key={item.id}>
|
||||
<TooltipTrigger>
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
aria-label={item.label}
|
||||
aria-pressed={isActive}
|
||||
@@ -116,48 +114,60 @@ function RightSidebarPanel() {
|
||||
if (!activeSidebar?.component) return null
|
||||
|
||||
return (
|
||||
<aside className="bg-background border-default flex h-full min-h-0 w-full flex-col overflow-hidden border-l">
|
||||
<aside className="bg-background border-default flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden border-l">
|
||||
{activeSidebar.component()}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
export function RightRailLayout({ children }: { children: ReactNode }) {
|
||||
export function RightPanelToolbarLayout({ children }: { children: ReactNode }) {
|
||||
const isMobile = useBreakpoint('md')
|
||||
const { activeSidebar } = useSidebarManagerSnapshot()
|
||||
const showRightSidebar = activeSidebar?.component !== undefined
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
{showRightSidebar ? (
|
||||
const main = <div className="h-full min-h-0 min-w-0 overflow-hidden">{children}</div>
|
||||
|
||||
if (isMobile) {
|
||||
return <div className="flex min-h-0 flex-1 flex-col overflow-hidden">{main}</div>
|
||||
}
|
||||
|
||||
if (showRightSidebar) {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
<ResizablePanelGroup
|
||||
orientation="horizontal"
|
||||
autoSaveId="default-layout-v2-right-sidebar"
|
||||
className="hidden md:flex min-h-0 min-w-0 flex-1 overflow-hidden"
|
||||
className="min-h-0 min-w-0 flex-1 overflow-hidden"
|
||||
>
|
||||
<ResizablePanel
|
||||
id="panel-v2-right-main-content"
|
||||
className="h-full min-h-0 min-w-0 overflow-hidden"
|
||||
className="min-h-0 min-w-0 overflow-hidden"
|
||||
>
|
||||
<div className="min-h-0 min-w-0 h-full">{children}</div>
|
||||
{main}
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle className="hidden md:flex bg-background" />
|
||||
<ResizableHandle withHandle className="bg-background" />
|
||||
<ResizablePanel
|
||||
id="panel-v2-right-sidebar"
|
||||
minSize={RIGHT_SIDEBAR_MIN_SIZE_PERCENTAGE}
|
||||
maxSize={RIGHT_SIDEBAR_MAX_SIZE_PERCENTAGE}
|
||||
defaultSize={RIGHT_SIDEBAR_DEFAULT_SIZE_PERCENTAGE}
|
||||
className="h-full min-h-0 overflow-hidden bg-background"
|
||||
className="flex min-h-0 overflow-hidden bg-background"
|
||||
>
|
||||
<RightSidebarPanel />
|
||||
<div className="flex h-full min-h-0 w-full min-w-0">
|
||||
<RightSidebarPanel />
|
||||
<RightPanelToolbar />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
) : (
|
||||
<div className="min-h-0 min-w-0 flex-1">{children}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className="hidden md:flex md:shrink-0">
|
||||
<RightIconRail />
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
<div className="min-h-0 min-w-0 flex-1">{main}</div>
|
||||
<div className="hidden shrink-0 md:flex">
|
||||
<RightPanelToolbar />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -3,4 +3,4 @@ export { ProjectLayoutV2, ProjectLayoutV2WithAuth } from './ProjectLayout'
|
||||
export { AppSidebarV2 } from './AppSidebar'
|
||||
export { NavGroup } from './NavGroup'
|
||||
export type { NavGroupItem, NavGroupProps } from './NavGroup'
|
||||
export { RightRailLayout } from './RightIconRail'
|
||||
export { RightPanelToolbarLayout } from './RightPanelToolbar'
|
||||
@@ -413,7 +413,7 @@ const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group"
|
||||
className={cn('relative flex w-full min-w-0 flex-col px-2 py-1', className)}
|
||||
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -503,7 +503,7 @@ SidebarMenuItem.displayName = 'SidebarMenuItem'
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
cn(
|
||||
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md py-2 px-1.5 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0 text-foreground-lighter data-[active=true]:text-foreground'
|
||||
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md py-2 px-1.5 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0 text-foreground-lighter data-[active=true]:text-foreground'
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
|
||||
Reference in new issue
Block a user