mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
refactor AppSidebarV2
This commit is contained in:
5 files changed
+451
-270
No files matched your search
@@ -1,283 +1,41 @@
|
||||
import { useFlag, useIsMFAEnabled, useParams } from 'common'
|
||||
import {
|
||||
useIsColumnLevelPrivilegesEnabled,
|
||||
useUnifiedLogsPreview,
|
||||
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { Connect } from 'components/interfaces/Connect/Connect'
|
||||
import { ConnectSheet } from 'components/interfaces/ConnectSheet/ConnectSheet'
|
||||
import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication/useIsETLPrivateAlpha'
|
||||
import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations'
|
||||
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { usePHFlag } from 'hooks/ui/useFlag'
|
||||
import { Home } from 'icons'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import {
|
||||
Blocks,
|
||||
Boxes,
|
||||
ChartArea,
|
||||
Compass,
|
||||
Plug,
|
||||
Receipt,
|
||||
Search,
|
||||
Settings,
|
||||
Users,
|
||||
} from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { Plug, Search } from 'lucide-react'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { Button, Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from 'ui'
|
||||
import { useSetCommandMenuOpen } from 'ui-patterns'
|
||||
|
||||
import { OrgSelector } from '../Navigation/NavigationBar/OrgSelector'
|
||||
import { ProjectBranchSelector } from '../Navigation/NavigationBar/ProjectBranchSelector'
|
||||
import { useGenerateSettingsMenu } from '../ProjectSettingsLayout/SettingsMenu.utils'
|
||||
import { NavGroup } from './NavGroup'
|
||||
import {
|
||||
generateDatabaseNavItems,
|
||||
generateObservabilityNavItems,
|
||||
generatePlatformNavItems,
|
||||
} from './NavigationV2.utils'
|
||||
import { NavUser } from './NavUser'
|
||||
import { type AppSidebarScope, useAppSidebarNavItems } from './useAppSidebarNavItems'
|
||||
|
||||
export type AppSidebarScope = 'project' | 'organization'
|
||||
export type { AppSidebarScope }
|
||||
|
||||
interface AppSidebarV2Props {
|
||||
scope?: AppSidebarScope
|
||||
}
|
||||
|
||||
export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef, slug } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
const setCommandMenuOpen = useSetCommandMenuOpen()
|
||||
const isUserMFAEnabled = useIsMFAEnabled()
|
||||
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
|
||||
const connectSheetFlag = usePHFlag<string | boolean>('connectSheet')
|
||||
const isFlagResolved = connectSheetFlag !== undefined
|
||||
const isConnectSheetEnabled = connectSheetFlag === true || connectSheetFlag === 'variation'
|
||||
|
||||
const currentScope =
|
||||
scope ?? (router.pathname.startsWith('/project') ? 'project' : 'organization')
|
||||
const isProjectScope = currentScope === 'project'
|
||||
|
||||
const ref = projectRef ?? project?.ref ?? ''
|
||||
const organizationSlug = slug ?? (router.query.orgSlug as string | undefined) ?? ''
|
||||
|
||||
const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
|
||||
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
|
||||
|
||||
const settingsMenu = useGenerateSettingsMenu()
|
||||
|
||||
// Database flags
|
||||
const { data: extensions } = useDatabaseExtensionsQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
},
|
||||
{
|
||||
enabled: isProjectScope,
|
||||
}
|
||||
)
|
||||
const { data: addons } = useProjectAddonsQuery(
|
||||
{ projectRef: project?.ref },
|
||||
{ enabled: isProjectScope }
|
||||
)
|
||||
|
||||
const pgNetExtensionExists = (extensions ?? []).find((ext) => ext.name === 'pg_net') !== undefined
|
||||
const pitrEnabled = addons?.selected_addons.find((addon) => addon.type === 'pitr') !== undefined
|
||||
const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
|
||||
const enablePgReplicate = useIsETLPrivateAlpha()
|
||||
|
||||
const {
|
||||
databaseReplication: showPgReplicate,
|
||||
databaseRoles: showRoles,
|
||||
integrationsWrappers: showWrappers,
|
||||
} = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers'])
|
||||
|
||||
// Platform flags
|
||||
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')
|
||||
|
||||
// Other flags
|
||||
const showReports = useIsFeatureEnabled('reports:all')
|
||||
const showBilling = useIsFeatureEnabled('billing:all')
|
||||
const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
|
||||
|
||||
const disableAccessMfa =
|
||||
selectedOrganization?.organization_requires_mfa === true && !isUserMFAEnabled
|
||||
const buildOrgHref = (href: string) => (disableAccessMfa ? router.asPath : href)
|
||||
|
||||
const projectSettingsItems =
|
||||
isProjectScope && ref
|
||||
? settingsMenu
|
||||
.flatMap((group) => {
|
||||
if (group.title === 'Billing' && !selectedOrganization?.slug) return []
|
||||
return group.items
|
||||
})
|
||||
.filter((item) => !item.disabled)
|
||||
.map((item) => ({
|
||||
title: item.name,
|
||||
url: item.url,
|
||||
}))
|
||||
: []
|
||||
|
||||
const isProjectHome =
|
||||
router.pathname === `/project/[ref]` || router.pathname === `/project/${ref}`
|
||||
const isProjectSettings = router.pathname.includes('/settings')
|
||||
|
||||
const projectItems = isProjectScope
|
||||
? [
|
||||
{
|
||||
title: 'Home',
|
||||
url: ref ? `/project/${ref}` : '/project',
|
||||
icon: Home,
|
||||
isActive: isProjectHome,
|
||||
},
|
||||
{
|
||||
title: 'Project Settings',
|
||||
url:
|
||||
ref && IS_PLATFORM
|
||||
? `/project/${ref}/settings/general`
|
||||
: ref
|
||||
? `/project/${ref}/settings/log-drains`
|
||||
: '/project',
|
||||
icon: Settings,
|
||||
items: projectSettingsItems.map((item) => ({
|
||||
...item,
|
||||
isActive: router.pathname.includes(item.url),
|
||||
})),
|
||||
isActive: isProjectSettings,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
const databaseItems =
|
||||
isProjectScope && ref
|
||||
? generateDatabaseNavItems(ref, project, {
|
||||
pgNetExtensionExists,
|
||||
pitrEnabled,
|
||||
columnLevelPrivileges,
|
||||
showPgReplicate,
|
||||
enablePgReplicate,
|
||||
showRoles,
|
||||
showWrappers,
|
||||
pathname: router.pathname,
|
||||
})
|
||||
: []
|
||||
|
||||
const platformItems =
|
||||
isProjectScope && ref
|
||||
? generatePlatformNavItems(ref, project, {
|
||||
authEnabled,
|
||||
edgeFunctionsEnabled,
|
||||
storageEnabled,
|
||||
realtimeEnabled,
|
||||
authOverviewPageEnabled,
|
||||
pathname: router.pathname,
|
||||
})
|
||||
: []
|
||||
|
||||
const observabilityItems =
|
||||
isProjectScope && ref
|
||||
? generateObservabilityNavItems(ref, project, {
|
||||
showReports,
|
||||
unifiedLogs: isUnifiedLogsEnabled,
|
||||
pathname: router.pathname,
|
||||
})
|
||||
: []
|
||||
|
||||
const { installedIntegrations } = useInstalledIntegrations()
|
||||
|
||||
const integrationsItems =
|
||||
isProjectScope && ref
|
||||
? [
|
||||
{
|
||||
title: 'Explore',
|
||||
url: isProjectBuilding ? `/project/${ref}` : `/project/${ref}/integrations`,
|
||||
icon: Compass,
|
||||
isActive:
|
||||
router.pathname === `/project/[ref]/integrations` ||
|
||||
(router.pathname.includes('/integrations') &&
|
||||
!installedIntegrations.some((int) =>
|
||||
router.pathname.includes(`/integrations/${int.id}`)
|
||||
)),
|
||||
},
|
||||
...installedIntegrations.map((integration) => ({
|
||||
title: integration.name,
|
||||
label: integration.status,
|
||||
url: `/project/${ref}/integrations/${integration.id}/overview`,
|
||||
icon: Blocks,
|
||||
isActive: router.pathname.includes(`/integrations/${integration.id}`),
|
||||
})),
|
||||
]
|
||||
: []
|
||||
|
||||
const activeOrganizationRoute = router.pathname.split('/')[3]
|
||||
const organizationItems =
|
||||
organizationSlug.length > 0
|
||||
? [
|
||||
{
|
||||
title: 'Projects',
|
||||
url: buildOrgHref(`/org/${organizationSlug}`),
|
||||
icon: Boxes,
|
||||
isActive: activeOrganizationRoute === undefined,
|
||||
},
|
||||
{
|
||||
title: 'Team',
|
||||
url: buildOrgHref(`/org/${organizationSlug}/team`),
|
||||
icon: Users,
|
||||
isActive: activeOrganizationRoute === 'team',
|
||||
},
|
||||
{
|
||||
title: 'Integrations',
|
||||
url: buildOrgHref(`/org/${organizationSlug}/integrations`),
|
||||
icon: Blocks,
|
||||
isActive: activeOrganizationRoute === 'integrations',
|
||||
},
|
||||
{
|
||||
title: 'Usage',
|
||||
url: buildOrgHref(`/org/${organizationSlug}/usage`),
|
||||
icon: ChartArea,
|
||||
isActive: activeOrganizationRoute === 'usage',
|
||||
},
|
||||
...(showBilling
|
||||
? [
|
||||
{
|
||||
title: 'Billing',
|
||||
url: buildOrgHref(`/org/${organizationSlug}/billing`),
|
||||
icon: Receipt,
|
||||
isActive: activeOrganizationRoute === 'billing',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'Organization Settings',
|
||||
url: buildOrgHref(`/org/${organizationSlug}/general`),
|
||||
icon: Settings,
|
||||
isActive:
|
||||
router.pathname.includes('/general') ||
|
||||
router.pathname.includes('/apps') ||
|
||||
router.pathname.includes('/audit') ||
|
||||
router.pathname.includes('/documents') ||
|
||||
router.pathname.includes('/security') ||
|
||||
router.pathname.includes('/sso'),
|
||||
},
|
||||
]
|
||||
: []
|
||||
isProjectScope,
|
||||
isActiveHealthy,
|
||||
projectItems,
|
||||
databaseItems,
|
||||
platformItems,
|
||||
observabilityItems,
|
||||
integrationsItems,
|
||||
organizationItems,
|
||||
} = useAppSidebarNavItems({ scope })
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -293,9 +51,7 @@ export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
|
||||
<Button
|
||||
type="outline"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setCommandMenuOpen(true)
|
||||
}}
|
||||
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} />}
|
||||
/>
|
||||
@@ -303,9 +59,7 @@ export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
|
||||
type="default"
|
||||
size="small"
|
||||
disabled={!isActiveHealthy}
|
||||
onClick={() => {
|
||||
setShowConnect(true)
|
||||
}}
|
||||
onClick={() => setShowConnect(true)}
|
||||
className="h-8 flex-1 justify-center gap-0 pl-2"
|
||||
icon={<Plug className="rotate-90" strokeWidth={1.5} />}
|
||||
>
|
||||
|
||||
@@ -41,16 +41,11 @@ export interface NavGroupProps {
|
||||
}
|
||||
|
||||
export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupProps) {
|
||||
// Load the sidebar state from localStorage using react-query for cross-component sync
|
||||
const [sidebarState, setSidebarState] = useLocalStorageQuery<Record<string, boolean>>(
|
||||
LOCAL_STORAGE_KEYS.DASHBOARD_SIDEBAR_STATE,
|
||||
{}
|
||||
)
|
||||
|
||||
// Get the current group's open state, defaulting to true (open)
|
||||
const isOpen = id ? sidebarState[id] ?? true : true
|
||||
|
||||
// Handler to update the open state for this specific group
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (id && isCollapsible) {
|
||||
setSidebarState((prev) => ({ ...prev, [id]: open }))
|
||||
|
||||
@@ -17,10 +17,8 @@ import {
|
||||
FileText,
|
||||
Lightbulb,
|
||||
Network,
|
||||
ScrollText,
|
||||
Settings,
|
||||
Telescope,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
|
||||
import type { NavGroupItem } from './NavGroup'
|
||||
@@ -62,13 +60,11 @@ export function generateDatabaseNavItems(
|
||||
const buildingUrl = `/project/${ref}`
|
||||
|
||||
const {
|
||||
pgNetExtensionExists,
|
||||
pitrEnabled,
|
||||
columnLevelPrivileges,
|
||||
showPgReplicate,
|
||||
enablePgReplicate,
|
||||
showRoles,
|
||||
showWrappers,
|
||||
pathname = '',
|
||||
} = flags || {}
|
||||
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
import { useFlag, useIsMFAEnabled, useParams } from 'common'
|
||||
import {
|
||||
useIsColumnLevelPrivilegesEnabled,
|
||||
useUnifiedLogsPreview,
|
||||
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication/useIsETLPrivateAlpha'
|
||||
import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations'
|
||||
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { Home } from 'icons'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import {
|
||||
Blocks,
|
||||
Boxes,
|
||||
ChartArea,
|
||||
Compass,
|
||||
Receipt,
|
||||
Settings,
|
||||
Users,
|
||||
} from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useGenerateSettingsMenu } from '../ProjectSettingsLayout/SettingsMenu.utils'
|
||||
import type { NavGroupItem } from './NavGroup'
|
||||
import {
|
||||
generateDatabaseNavItems,
|
||||
generateObservabilityNavItems,
|
||||
generatePlatformNavItems,
|
||||
} from './NavigationV2.utils'
|
||||
|
||||
export type AppSidebarScope = 'project' | 'organization'
|
||||
|
||||
export interface UseAppSidebarNavItemsParams {
|
||||
scope?: AppSidebarScope
|
||||
}
|
||||
|
||||
export interface UseAppSidebarNavItemsResult {
|
||||
ref: string
|
||||
organizationSlug: string
|
||||
isProjectScope: boolean
|
||||
isActiveHealthy: boolean
|
||||
isProjectBuilding: boolean
|
||||
projectItems: NavGroupItem[]
|
||||
databaseItems: NavGroupItem[]
|
||||
platformItems: NavGroupItem[]
|
||||
observabilityItems: NavGroupItem[]
|
||||
integrationsItems: NavGroupItem[]
|
||||
organizationItems: NavGroupItem[]
|
||||
}
|
||||
|
||||
export function useAppSidebarNavItems(
|
||||
params: UseAppSidebarNavItemsParams = {}
|
||||
): UseAppSidebarNavItemsResult {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef, slug } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
const isUserMFAEnabled = useIsMFAEnabled()
|
||||
|
||||
const currentScope: AppSidebarScope =
|
||||
params.scope ?? (router.pathname.startsWith('/project') ? 'project' : 'organization')
|
||||
const isProjectScope = currentScope === 'project'
|
||||
|
||||
const ref = projectRef ?? project?.ref ?? ''
|
||||
const organizationSlug = slug ?? (router.query.orgSlug as string | undefined) ?? ''
|
||||
|
||||
const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
|
||||
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
|
||||
|
||||
const settingsMenu = useGenerateSettingsMenu()
|
||||
|
||||
const { data: extensions } = useDatabaseExtensionsQuery(
|
||||
{ projectRef: project?.ref, connectionString: project?.connectionString },
|
||||
{ enabled: isProjectScope }
|
||||
)
|
||||
const { data: addons } = useProjectAddonsQuery(
|
||||
{ projectRef: project?.ref },
|
||||
{ enabled: isProjectScope }
|
||||
)
|
||||
|
||||
const pgNetExtensionExists =
|
||||
(extensions ?? []).find((ext) => ext.name === 'pg_net') !== undefined
|
||||
const pitrEnabled = addons?.selected_addons.find((addon) => addon.type === 'pitr') !== undefined
|
||||
|
||||
const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
|
||||
const enablePgReplicate = useIsETLPrivateAlpha()
|
||||
|
||||
const {
|
||||
databaseReplication: showPgReplicate,
|
||||
databaseRoles: showRoles,
|
||||
integrationsWrappers: showWrappers,
|
||||
} = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers'])
|
||||
|
||||
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 showBilling = useIsFeatureEnabled('billing:all')
|
||||
const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
|
||||
|
||||
const disableAccessMfa =
|
||||
selectedOrganization?.organization_requires_mfa === true && !isUserMFAEnabled
|
||||
const buildOrgHref = (href: string) => (disableAccessMfa ? router.asPath : href)
|
||||
|
||||
const projectSettingsItems =
|
||||
isProjectScope && ref
|
||||
? settingsMenu
|
||||
.flatMap((group) => {
|
||||
if (group.title === 'Billing' && !selectedOrganization?.slug) return []
|
||||
return group.items
|
||||
})
|
||||
.filter((item) => !item.disabled)
|
||||
.map((item) => ({ title: item.name, url: item.url }))
|
||||
: []
|
||||
|
||||
const isProjectHome =
|
||||
router.pathname === `/project/[ref]` || router.pathname === `/project/${ref}`
|
||||
const isProjectSettings = router.pathname.includes('/settings')
|
||||
|
||||
const projectItems: NavGroupItem[] = isProjectScope
|
||||
? [
|
||||
{
|
||||
title: 'Home',
|
||||
url: ref ? `/project/${ref}` : '/project',
|
||||
icon: Home,
|
||||
isActive: isProjectHome,
|
||||
},
|
||||
{
|
||||
title: 'Project Settings',
|
||||
url:
|
||||
ref && IS_PLATFORM
|
||||
? `/project/${ref}/settings/general`
|
||||
: ref
|
||||
? `/project/${ref}/settings/log-drains`
|
||||
: '/project',
|
||||
icon: Settings,
|
||||
items: projectSettingsItems.map((item) => ({
|
||||
...item,
|
||||
isActive: router.pathname.includes(item.url),
|
||||
})),
|
||||
isActive: isProjectSettings,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
const databaseItems: NavGroupItem[] =
|
||||
isProjectScope && ref
|
||||
? generateDatabaseNavItems(ref, project, {
|
||||
pgNetExtensionExists,
|
||||
pitrEnabled,
|
||||
columnLevelPrivileges,
|
||||
showPgReplicate,
|
||||
enablePgReplicate,
|
||||
showRoles,
|
||||
showWrappers,
|
||||
pathname: router.pathname,
|
||||
})
|
||||
: []
|
||||
|
||||
const platformItems: NavGroupItem[] =
|
||||
isProjectScope && ref
|
||||
? generatePlatformNavItems(ref, project, {
|
||||
authEnabled,
|
||||
edgeFunctionsEnabled,
|
||||
storageEnabled,
|
||||
realtimeEnabled,
|
||||
authOverviewPageEnabled,
|
||||
pathname: router.pathname,
|
||||
})
|
||||
: []
|
||||
|
||||
const observabilityItems: NavGroupItem[] =
|
||||
isProjectScope && ref
|
||||
? generateObservabilityNavItems(ref, project, {
|
||||
showReports,
|
||||
unifiedLogs: isUnifiedLogsEnabled,
|
||||
pathname: router.pathname,
|
||||
})
|
||||
: []
|
||||
|
||||
const { installedIntegrations } = useInstalledIntegrations()
|
||||
|
||||
const integrationsItems: NavGroupItem[] =
|
||||
isProjectScope && ref
|
||||
? [
|
||||
{
|
||||
title: 'Explore',
|
||||
url: isProjectBuilding ? `/project/${ref}` : `/project/${ref}/integrations`,
|
||||
icon: Compass,
|
||||
isActive:
|
||||
router.pathname === `/project/[ref]/integrations` ||
|
||||
(router.pathname.includes('/integrations') &&
|
||||
!installedIntegrations.some((int) =>
|
||||
router.pathname.includes(`/integrations/${int.id}`)
|
||||
)),
|
||||
},
|
||||
...installedIntegrations.map((integration) => ({
|
||||
title: integration.name,
|
||||
label: integration.status,
|
||||
url: `/project/${ref}/integrations/${integration.id}/overview`,
|
||||
icon: Blocks,
|
||||
isActive: router.pathname.includes(`/integrations/${integration.id}`),
|
||||
})),
|
||||
]
|
||||
: []
|
||||
|
||||
const activeOrganizationRoute = router.pathname.split('/')[3]
|
||||
|
||||
const organizationItems: NavGroupItem[] =
|
||||
organizationSlug.length > 0
|
||||
? [
|
||||
{
|
||||
title: 'Projects',
|
||||
url: buildOrgHref(`/org/${organizationSlug}`),
|
||||
icon: Boxes,
|
||||
isActive: activeOrganizationRoute === undefined,
|
||||
},
|
||||
{
|
||||
title: 'Team',
|
||||
url: buildOrgHref(`/org/${organizationSlug}/team`),
|
||||
icon: Users,
|
||||
isActive: activeOrganizationRoute === 'team',
|
||||
},
|
||||
{
|
||||
title: 'Integrations',
|
||||
url: buildOrgHref(`/org/${organizationSlug}/integrations`),
|
||||
icon: Blocks,
|
||||
isActive: activeOrganizationRoute === 'integrations',
|
||||
},
|
||||
{
|
||||
title: 'Usage',
|
||||
url: buildOrgHref(`/org/${organizationSlug}/usage`),
|
||||
icon: ChartArea,
|
||||
isActive: activeOrganizationRoute === 'usage',
|
||||
},
|
||||
...(showBilling
|
||||
? [
|
||||
{
|
||||
title: 'Billing',
|
||||
url: buildOrgHref(`/org/${organizationSlug}/billing`),
|
||||
icon: Receipt,
|
||||
isActive: activeOrganizationRoute === 'billing',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'Organization Settings',
|
||||
url: buildOrgHref(`/org/${organizationSlug}/general`),
|
||||
icon: Settings,
|
||||
isActive:
|
||||
router.pathname.includes('/general') ||
|
||||
router.pathname.includes('/apps') ||
|
||||
router.pathname.includes('/audit') ||
|
||||
router.pathname.includes('/documents') ||
|
||||
router.pathname.includes('/security') ||
|
||||
router.pathname.includes('/sso'),
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
return {
|
||||
ref,
|
||||
organizationSlug,
|
||||
isProjectScope,
|
||||
isActiveHealthy,
|
||||
isProjectBuilding,
|
||||
projectItems,
|
||||
databaseItems,
|
||||
platformItems,
|
||||
observabilityItems,
|
||||
integrationsItems,
|
||||
organizationItems,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useAppSidebarNavItems } from 'components/layouts/NavigationV2/useAppSidebarNavItems'
|
||||
|
||||
// ─── Module mocks ─────────────────────────────────────────────────────────────
|
||||
|
||||
vi.mock('lib/constants', async () => {
|
||||
const actual = await vi.importActual<Record<string, unknown>>('lib/constants')
|
||||
return { ...actual, IS_PLATFORM: true }
|
||||
})
|
||||
|
||||
vi.mock('next/router', () => ({
|
||||
useRouter: vi.fn().mockReturnValue({
|
||||
pathname: '/project/[ref]',
|
||||
asPath: '/project/test-ref',
|
||||
query: {},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('common', () => ({
|
||||
useFlag: vi.fn().mockReturnValue(false),
|
||||
useParams: vi.fn().mockReturnValue({ ref: 'test-ref', slug: undefined }),
|
||||
useIsMFAEnabled: vi.fn().mockReturnValue(false),
|
||||
}))
|
||||
|
||||
vi.mock('hooks/misc/useSelectedOrganization', () => ({
|
||||
useSelectedOrganizationQuery: vi
|
||||
.fn()
|
||||
.mockReturnValue({ data: { slug: 'my-org', organization_requires_mfa: false } }),
|
||||
}))
|
||||
|
||||
vi.mock('hooks/misc/useSelectedProject', () => ({
|
||||
useSelectedProjectQuery: vi
|
||||
.fn()
|
||||
.mockReturnValue({ data: { ref: 'test-ref', status: 'ACTIVE_HEALTHY' } }),
|
||||
}))
|
||||
|
||||
vi.mock('hooks/misc/useIsFeatureEnabled', () => ({
|
||||
useIsFeatureEnabled: vi.fn((flags: string | string[]) => {
|
||||
if (typeof flags === 'string') return true
|
||||
return Object.fromEntries((flags as string[]).map((f) => [f.replace(/[:/]/g, ''), true]))
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('components/interfaces/App/FeaturePreview/FeaturePreviewContext', () => ({
|
||||
useIsColumnLevelPrivilegesEnabled: vi.fn().mockReturnValue(false),
|
||||
useUnifiedLogsPreview: vi.fn().mockReturnValue({ isEnabled: false }),
|
||||
}))
|
||||
|
||||
vi.mock('components/interfaces/Database/Replication/useIsETLPrivateAlpha', () => ({
|
||||
useIsETLPrivateAlpha: vi.fn().mockReturnValue(false),
|
||||
}))
|
||||
|
||||
vi.mock('components/interfaces/Integrations/Landing/useInstalledIntegrations', () => ({
|
||||
useInstalledIntegrations: vi.fn().mockReturnValue({ installedIntegrations: [] }),
|
||||
}))
|
||||
|
||||
vi.mock('data/database-extensions/database-extensions-query', () => ({
|
||||
useDatabaseExtensionsQuery: vi.fn().mockReturnValue({ data: undefined }),
|
||||
}))
|
||||
|
||||
vi.mock('data/subscriptions/project-addons-query', () => ({
|
||||
useProjectAddonsQuery: vi.fn().mockReturnValue({ data: undefined }),
|
||||
}))
|
||||
|
||||
vi.mock('components/layouts/ProjectSettingsLayout/SettingsMenu.utils', () => ({
|
||||
useGenerateSettingsMenu: vi.fn().mockReturnValue([]),
|
||||
}))
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('useAppSidebarNavItems', () => {
|
||||
describe('scope detection', () => {
|
||||
it('detects project scope from pathname', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems())
|
||||
expect(result.current.isProjectScope).toBe(true)
|
||||
})
|
||||
|
||||
it('respects explicit "organization" scope override', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'organization' }))
|
||||
expect(result.current.isProjectScope).toBe(false)
|
||||
})
|
||||
|
||||
it('respects explicit "project" scope override', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'project' }))
|
||||
expect(result.current.isProjectScope).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectItems', () => {
|
||||
it('includes Home and Project Settings when in project scope', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'project' }))
|
||||
const titles = result.current.projectItems.map((i) => i.title)
|
||||
expect(titles).toContain('Home')
|
||||
expect(titles).toContain('Project Settings')
|
||||
})
|
||||
|
||||
it('returns empty projectItems in organization scope', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'organization' }))
|
||||
expect(result.current.projectItems).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('Home item is active when on the project home route', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'project' }))
|
||||
const home = result.current.projectItems.find((i) => i.title === 'Home')
|
||||
expect(home?.isActive).toBe(true)
|
||||
})
|
||||
|
||||
it('Home url points to the current project ref', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'project' }))
|
||||
const home = result.current.projectItems.find((i) => i.title === 'Home')
|
||||
expect(home?.url).toBe('/project/test-ref')
|
||||
})
|
||||
|
||||
it('Project Settings url uses general settings on IS_PLATFORM', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'project' }))
|
||||
const settings = result.current.projectItems.find((i) => i.title === 'Project Settings')
|
||||
expect(settings?.url).toBe('/project/test-ref/settings/general')
|
||||
})
|
||||
})
|
||||
|
||||
describe('organizationItems', () => {
|
||||
it('returns empty when no organizationSlug is available', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'organization' }))
|
||||
// useParams returns slug: undefined, so organizationSlug = ''
|
||||
expect(result.current.organizationItems).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isActiveHealthy', () => {
|
||||
it('returns true when project status is ACTIVE_HEALTHY', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems())
|
||||
expect(result.current.isActiveHealthy).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ref and organizationSlug', () => {
|
||||
it('exposes the current project ref', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems())
|
||||
expect(result.current.ref).toBe('test-ref')
|
||||
})
|
||||
|
||||
it('exposes an empty organizationSlug when none is set', () => {
|
||||
const { result } = renderHook(() => useAppSidebarNavItems())
|
||||
expect(result.current.organizationSlug).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user