diff --git a/apps/studio/components/interfaces/UserDropdown.tsx b/apps/studio/components/interfaces/UserDropdown.tsx index 5ae1301019b..cb0cd4bcbcd 100644 --- a/apps/studio/components/interfaces/UserDropdown.tsx +++ b/apps/studio/components/interfaces/UserDropdown.tsx @@ -39,7 +39,7 @@ export function UserDropdown({ - - )} + {showSelector && + (isProjectScope ? ( + + ) : ( + selectedOrganization && + ))} +
+ +
- + {isProjectScope ? ( <> @@ -75,26 +70,25 @@ export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) { +
+
- ) : selectedOrganization ? ( + ) : ( - ) : null} + )} -
-
- {IS_PLATFORM && } diff --git a/apps/studio/components/layouts/NavigationV2/DefaultLayout.tsx b/apps/studio/components/layouts/NavigationV2/DefaultLayout.tsx index d97b215dcc0..5db145bc981 100644 --- a/apps/studio/components/layouts/NavigationV2/DefaultLayout.tsx +++ b/apps/studio/components/layouts/NavigationV2/DefaultLayout.tsx @@ -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) => { 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 = ( + + ) + + const contentRow = {children} + + const mainColumn = ( +
+
{header}
+
{contentRow}
+
+ ) + return ( -
+
-
+
- +
{showLeftSidebar ? ( - -
{children}
+ + {mainColumn}
) : ( -
{children}
+ mainColumn )} - +
) diff --git a/apps/studio/components/layouts/NavigationV2/LayoutHeader.tsx b/apps/studio/components/layouts/NavigationV2/LayoutHeader.tsx new file mode 100644 index 00000000000..5b764e09465 --- /dev/null +++ b/apps/studio/components/layouts/NavigationV2/LayoutHeader.tsx @@ -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 ( + <> +
+ {backToDashboardURL && isAccountPage && ( +
+ + + +
+ )} +
+
+ {/* + */} + {/* {IS_PLATFORM && ( +
+ {isProjectScope ? ( + + ) : ( + selectedOrganization && + )} +
+ )} */} + + + {headerTitle && ( + + {/* */} + + {headerTitle} + + )} + + + + + {exceedingLimits && ( +
+ + Exceeding usage limits + +
+ )} +
+ {/*
+ div]:border-none', + '[&_.command-shortcut>div]:pr-2', + '[&_.command-shortcut>div]:bg-transparent', + '[&_.command-shortcut>div]:text-foreground-lighter' + )} + /> +
*/} +
+ {customHeaderComponents && customHeaderComponents} + {IS_PLATFORM ? ( + <> + + + div]:border-none', + '[&_.command-shortcut>div]:pr-2', + '[&_.command-shortcut>div]:bg-transparent', + '[&_.command-shortcut>div]:text-foreground-lighter' + )} + /> + + + ) : ( + <> + + div]:border-none', + '[&_.command-shortcut>div]:pr-2', + '[&_.command-shortcut>div]:bg-transparent', + '[&_.command-shortcut>div]:text-foreground-lighter' + )} + /> + + + )} +
+
+
+ + + + ) +} diff --git a/apps/studio/components/layouts/NavigationV2/NavGroup.tsx b/apps/studio/components/layouts/NavigationV2/NavGroup.tsx index 278b4c61654..64c05fe3dc4 100644 --- a/apps/studio/components/layouts/NavigationV2/NavGroup.tsx +++ b/apps/studio/components/layouts/NavigationV2/NavGroup.tsx @@ -65,14 +65,13 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro {item.title} {item.label && ( - + {item.label} )} @@ -100,16 +99,15 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro ) : ( {item.title} {item.label && ( - + {item.label} )} @@ -131,7 +129,7 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro @@ -139,7 +137,7 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro {isCollapsible && ( )} diff --git a/apps/studio/components/layouts/NavigationV2/NavUser.tsx b/apps/studio/components/layouts/NavigationV2/NavUser.tsx deleted file mode 100644 index 167195bdccf..00000000000 --- a/apps/studio/components/layouts/NavigationV2/NavUser.tsx +++ /dev/null @@ -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 ( - - - - - - {isLoading ? ( -
- -
- ) : ( - - )} -
- {username} - {primaryEmail !== username && profileShowEmailEnabled && ( - {primaryEmail} - )} -
- -
-
- - - -
-
-
- ) -} diff --git a/apps/studio/components/layouts/NavigationV2/RightIconRail.tsx b/apps/studio/components/layouts/NavigationV2/RightPanelToolbar.tsx similarity index 77% rename from apps/studio/components/layouts/NavigationV2/RightIconRail.tsx rename to apps/studio/components/layouts/NavigationV2/RightPanelToolbar.tsx index 4898d564175..af0ab394b4f 100644 --- a/apps/studio/components/layouts/NavigationV2/RightIconRail.tsx +++ b/apps/studio/components/layouts/NavigationV2/RightPanelToolbar.tsx @@ -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: () => , }, { - // id: SIDEBAR_KEYS.SUPPORT_PANEL, id: SIDEBAR_KEYS.HELP_PANEL, label: 'Help & Support', icon: () => , @@ -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 (