mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## Context Related to dashboard scalability Previous PR: https://github.com/supabase/supabase/pull/42856 Note: Changes are all feature flagged still and I'm still not entirely convinced with the current UX Will iterate as as go along, and only make this publicly available when we're satisfied with the behaviour Adds a "Dashboard preference" section to the project settings <img width="265" height="740" alt="image" src="https://github.com/user-attachments/assets/6ce1aa19-26c2-47c6-a9c4-595137266631" /> In which users can then select which database they'd like to use for read queries run from the dashboard Note: Everything is local storage for now, but we'd need middleware support if we want to make this setting persist for all users on the project <img width="791" height="434" alt="image" src="https://github.com/user-attachments/assets/e651d6d9-fed4-4da4-b552-c9f93f8d46d3" /> Added a dialog as well to further explain what this implies <img width="610" height="312" alt="image" src="https://github.com/user-attachments/assets/0aa957af-cb51-476f-aa79-8948a7cbe5ae" /> ## To test - Choosing a replica in dashboard preferences will only affect the table editor as thats the only place that's set up so far to use a replica for read queries (I'll need to follow up for other parts of the dashboard in subsequent PRs)
209 lines
7.1 KiB
TypeScript
209 lines
7.1 KiB
TypeScript
'use client'
|
|
|
|
import { useFlag, useParams } from 'common'
|
|
import {
|
|
useIsAPIDocsSidePanelEnabled,
|
|
useUnifiedLogsPreview,
|
|
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
|
import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar'
|
|
import {
|
|
generateOtherRoutes,
|
|
generateProductRoutes,
|
|
generateSettingsRoutes,
|
|
generateToolRoutes,
|
|
} from 'components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils'
|
|
import type { Route } from 'components/ui/ui.types'
|
|
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
|
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
|
import { Home } from 'icons'
|
|
import { getPathnameWithoutQuery, getPathSegment } from 'lib/pathname.utils'
|
|
import { ChevronLeft } from 'lucide-react'
|
|
import { useRouter } from 'next/router'
|
|
import React, { useMemo } from 'react'
|
|
import { Button, cn, Separator, SidebarGroup, SidebarMenu } from 'ui'
|
|
import { GenericSkeletonLoader } from 'ui-patterns'
|
|
|
|
import { resolveSectionDisplay } from './MobileMenuContent.utils'
|
|
import { getProductMenuComponent } from './mobileProductMenuRegistry'
|
|
import { TopLevelRouteItem } from './TopLevelRouteItem'
|
|
import { routeHasSubmenu, useMobileMenuNavigation } from './useMobileMenuNavigation'
|
|
|
|
export interface MobileMenuContentProps {
|
|
currentProductMenu: React.ReactNode
|
|
currentProduct: string
|
|
currentSectionKey: string | null
|
|
onCloseSheet?: () => void
|
|
}
|
|
|
|
export function MobileMenuContent({
|
|
currentProductMenu,
|
|
currentProduct,
|
|
currentSectionKey,
|
|
onCloseSheet,
|
|
}: MobileMenuContentProps) {
|
|
const router = useRouter()
|
|
const { ref } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const pathname = getPathnameWithoutQuery(router.asPath, router.pathname)
|
|
const activeRoute = getPathSegment(pathname, 3)
|
|
|
|
const { viewLevel, selectedSectionKey, handleTopLevelClick, handleBackToTop } =
|
|
useMobileMenuNavigation({
|
|
currentSectionKey,
|
|
hasCurrentProductMenu: !!currentProductMenu,
|
|
onCloseSheet,
|
|
})
|
|
|
|
const {
|
|
projectAuthAll: authEnabled,
|
|
projectEdgeFunctionAll: edgeFunctionsEnabled,
|
|
projectStorageAll: storageEnabled,
|
|
realtimeAll: realtimeEnabled,
|
|
} = useIsFeatureEnabled([
|
|
'project_auth:all',
|
|
'project_edge_function:all',
|
|
'project_storage:all',
|
|
'realtime:all',
|
|
])
|
|
const authOverviewPageEnabled = useFlag('authOverviewPage')
|
|
const showReports = useIsFeatureEnabled('reports:all')
|
|
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
|
|
const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
|
|
|
|
const toolRoutes = useMemo(() => generateToolRoutes(ref, project), [ref, project])
|
|
const productRoutes = useMemo(
|
|
() =>
|
|
generateProductRoutes(ref, project, {
|
|
auth: authEnabled,
|
|
edgeFunctions: edgeFunctionsEnabled,
|
|
storage: storageEnabled,
|
|
realtime: realtimeEnabled,
|
|
authOverviewPage: authOverviewPageEnabled,
|
|
}),
|
|
[
|
|
ref,
|
|
project,
|
|
authEnabled,
|
|
edgeFunctionsEnabled,
|
|
storageEnabled,
|
|
realtimeEnabled,
|
|
authOverviewPageEnabled,
|
|
]
|
|
)
|
|
const otherRoutes = useMemo(
|
|
() =>
|
|
generateOtherRoutes(ref, project, {
|
|
unifiedLogs: isUnifiedLogsEnabled,
|
|
showReports,
|
|
apiDocsSidePanel: isNewAPIDocsEnabled,
|
|
}),
|
|
[ref, project, isUnifiedLogsEnabled, showReports, isNewAPIDocsEnabled]
|
|
)
|
|
const settingsRoutes = useMemo(() => generateSettingsRoutes(ref), [ref])
|
|
|
|
const homeRoute: Route = useMemo(
|
|
() => ({
|
|
key: 'HOME',
|
|
label: 'Project Overview',
|
|
icon: <Home size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
|
link: ref ? `/project/${ref}` : undefined,
|
|
}),
|
|
[ref]
|
|
)
|
|
|
|
const allTopLevelRoutes = useMemo(
|
|
() => [homeRoute, ...toolRoutes, ...productRoutes, ...otherRoutes, ...settingsRoutes],
|
|
[homeRoute, toolRoutes, productRoutes, otherRoutes, settingsRoutes]
|
|
)
|
|
|
|
const { sectionKey: sectionKeyToShow, sectionLabel } = resolveSectionDisplay({
|
|
viewLevel,
|
|
selectedSectionKey,
|
|
currentSectionKey,
|
|
currentProduct,
|
|
routes: allTopLevelRoutes,
|
|
})
|
|
|
|
const SectionMenuContent = sectionKeyToShow ? getProductMenuComponent(sectionKeyToShow) : null
|
|
const pageSegment = getPathSegment(pathname, 4)
|
|
|
|
const renderRoute = (route: Route, isActive: boolean) => (
|
|
<TopLevelRouteItem
|
|
key={route.key}
|
|
route={route}
|
|
isActive={isActive}
|
|
hasSubmenu={routeHasSubmenu(route)}
|
|
onTopLevelClick={handleTopLevelClick}
|
|
onCloseSheet={onCloseSheet}
|
|
/>
|
|
)
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-background">
|
|
{viewLevel === 'section' && sectionLabel && (
|
|
<div
|
|
className={cn(
|
|
'flex-shrink-0 flex items-center gap-2 border-b border-default px-3 min-h-[var(--header-height)]'
|
|
)}
|
|
>
|
|
<Button
|
|
type="text"
|
|
className="!p-1 justify-start"
|
|
icon={<ChevronLeft size={20} />}
|
|
onClick={handleBackToTop}
|
|
aria-label="Back to menu"
|
|
block
|
|
>
|
|
<span className="font-medium truncate text-sm">{sectionLabel}</span>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<div className="flex-1 overflow-y-auto pb-8 text-sidebar-foreground">
|
|
{viewLevel === 'top' && (
|
|
<nav className="flex flex-col gap-2 p-1" aria-label="Project menu">
|
|
<SidebarMenu>
|
|
<SidebarGroup className="gap-0.5">
|
|
{[homeRoute, ...toolRoutes].map((route) =>
|
|
renderRoute(
|
|
route,
|
|
activeRoute === route.key || (route.key === 'HOME' && activeRoute === undefined)
|
|
)
|
|
)}
|
|
</SidebarGroup>
|
|
<Separator className="mx-2 w-auto bg-sidebar-border" />
|
|
<SidebarGroup className="gap-0.5">
|
|
{productRoutes.map((route) => renderRoute(route, activeRoute === route.key))}
|
|
</SidebarGroup>
|
|
<Separator className="mx-2 w-auto bg-sidebar-border" />
|
|
<SidebarGroup className="gap-0.5">
|
|
{otherRoutes.map((route) => renderRoute(route, activeRoute === route.key))}
|
|
</SidebarGroup>
|
|
<Separator className="mx-2 w-auto bg-sidebar-border" />
|
|
<SidebarGroup className="gap-0.5">
|
|
{settingsRoutes.map((route) => renderRoute(route, activeRoute === route.key))}
|
|
</SidebarGroup>
|
|
</SidebarMenu>
|
|
</nav>
|
|
)}
|
|
{viewLevel === 'section' && sectionKeyToShow && (
|
|
<div className="p-1">
|
|
{sectionKeyToShow === currentSectionKey && currentProductMenu ? (
|
|
currentProductMenu
|
|
) : SectionMenuContent ? (
|
|
<React.Suspense fallback={<GenericSkeletonLoader className="p-4" />}>
|
|
{sectionKeyToShow === 'advisors' ? (
|
|
<SectionMenuContent
|
|
{...({ page: pageSegment } as React.ComponentProps<typeof SectionMenuContent>)}
|
|
/>
|
|
) : (
|
|
<SectionMenuContent />
|
|
)}
|
|
</React.Suspense>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|