diff --git a/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.test.tsx b/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.test.tsx
new file mode 100644
index 0000000000..4518c37011
--- /dev/null
+++ b/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.test.tsx
@@ -0,0 +1,208 @@
+import type { Project } from 'data/projects/project-detail-query'
+import { describe, expect, it } from 'vitest'
+
+import {
+ generateOtherRoutes,
+ generateProductRoutes,
+ generateSettingsRoutes,
+ generateToolRoutes,
+} from './NavigationBar.utils'
+
+const REF = 'test-project-ref'
+
+const activeProject = { status: 'ACTIVE_HEALTHY' } as Project
+const buildingProject = { status: 'COMING_UP' } as Project
+const inactiveProject = { status: 'INACTIVE' } as Project
+
+const keys = (routes: { key: string }[]) => routes.map((r) => r.key)
+
+describe('generateToolRoutes', () => {
+ it('always returns Table Editor and SQL Editor', () => {
+ const routes = generateToolRoutes(REF, activeProject)
+ expect(keys(routes)).toEqual(['editor', 'sql'])
+ })
+
+ it('marks routes as disabled when project is not active', () => {
+ const routes = generateToolRoutes(REF, inactiveProject)
+ expect(routes.every((r) => r.disabled)).toBe(true)
+ })
+
+ it('points links to the building URL when project is building', () => {
+ const routes = generateToolRoutes(REF, buildingProject)
+ expect(routes.every((r) => r.link === `/project/${REF}`)).toBe(true)
+ })
+
+ it('returns links as false when ref is undefined', () => {
+ const routes = generateToolRoutes(undefined, activeProject)
+ expect(routes.every((r) => r.link === undefined)).toBe(true)
+ })
+})
+
+describe('generateProductRoutes', () => {
+ it('includes all product routes when all features are enabled', () => {
+ const routes = generateProductRoutes(REF, activeProject, {
+ auth: true,
+ storage: true,
+ edgeFunctions: true,
+ realtime: true,
+ })
+ expect(keys(routes)).toEqual(['database', 'auth', 'storage', 'functions', 'realtime'])
+ })
+
+ it('includes all product routes by default (features default to true)', () => {
+ const routes = generateProductRoutes(REF, activeProject)
+ expect(keys(routes)).toEqual(['database', 'auth', 'storage', 'functions', 'realtime'])
+ })
+
+ it('excludes auth when auth feature is disabled', () => {
+ const routes = generateProductRoutes(REF, activeProject, { auth: false })
+ expect(keys(routes)).not.toContain('auth')
+
+ expect(keys(routes)).toContain('database')
+ expect(keys(routes)).toContain('storage')
+ })
+
+ it('excludes storage when storage feature is disabled', () => {
+ const routes = generateProductRoutes(REF, activeProject, { storage: false })
+ expect(keys(routes)).not.toContain('storage')
+ })
+
+ it('excludes edge functions when edgeFunctions feature is disabled', () => {
+ const routes = generateProductRoutes(REF, activeProject, { edgeFunctions: false })
+ expect(keys(routes)).not.toContain('functions')
+ })
+
+ it('excludes realtime when realtime feature is disabled', () => {
+ const routes = generateProductRoutes(REF, activeProject, { realtime: false })
+ expect(keys(routes)).not.toContain('realtime')
+ })
+
+ it('links auth to overview page when authOverviewPage is enabled', () => {
+ const routes = generateProductRoutes(REF, activeProject, { authOverviewPage: true })
+ const authRoute = routes.find((r) => r.key === 'auth')
+ expect(authRoute?.link).toBe(`/project/${REF}/auth/overview`)
+ })
+
+ it('links auth to users page by default', () => {
+ const routes = generateProductRoutes(REF, activeProject)
+ const authRoute = routes.find((r) => r.key === 'auth')
+ expect(authRoute?.link).toBe(`/project/${REF}/auth/users`)
+ })
+
+ it('always includes database even when all optional features are disabled', () => {
+ const routes = generateProductRoutes(REF, activeProject, {
+ auth: false,
+ storage: false,
+ edgeFunctions: false,
+ realtime: false,
+ })
+ expect(keys(routes)).toEqual(['database'])
+ })
+})
+
+describe('generateOtherRoutes', () => {
+ it('always includes advisors, logs, and integrations', () => {
+ const routes = generateOtherRoutes(REF, activeProject, { isPlatform: true })
+ expect(keys(routes)).toContain('advisors')
+ expect(keys(routes)).toContain('logs')
+ expect(keys(routes)).toContain('integrations')
+ })
+
+ it('includes observability on platform when reports are enabled', () => {
+ const routes = generateOtherRoutes(REF, activeProject, {
+ isPlatform: true,
+ showReports: true,
+ })
+ expect(keys(routes)).toContain('observability')
+ })
+
+ it('excludes observability on platform when reports are disabled', () => {
+ const routes = generateOtherRoutes(REF, activeProject, {
+ isPlatform: true,
+ showReports: false,
+ })
+ expect(keys(routes)).not.toContain('observability')
+ })
+
+ it('excludes observability in self-hosted mode even when reports are enabled', () => {
+ const routes = generateOtherRoutes(REF, activeProject, {
+ isPlatform: false,
+ showReports: true,
+ })
+ expect(keys(routes)).not.toContain('observability')
+ })
+
+ it('excludes observability in self-hosted mode when reports are disabled', () => {
+ const routes = generateOtherRoutes(REF, activeProject, {
+ isPlatform: false,
+ showReports: false,
+ })
+ expect(keys(routes)).not.toContain('observability')
+ })
+
+ it('includes API Docs when apiDocsSidePanel is enabled', () => {
+ const routes = generateOtherRoutes(REF, activeProject, {
+ isPlatform: true,
+ apiDocsSidePanel: true,
+ })
+ expect(keys(routes)).toContain('api')
+ })
+
+ it('excludes API Docs when apiDocsSidePanel is disabled', () => {
+ const routes = generateOtherRoutes(REF, activeProject, {
+ isPlatform: true,
+ apiDocsSidePanel: false,
+ })
+ expect(keys(routes)).not.toContain('api')
+ })
+
+ it('links logs to unified logs page when unifiedLogs is enabled', () => {
+ const routes = generateOtherRoutes(REF, activeProject, {
+ isPlatform: true,
+ unifiedLogs: true,
+ })
+ const logsRoute = routes.find((r) => r.key === 'logs')
+ expect(logsRoute?.link).toBe(`/project/${REF}/logs`)
+ })
+
+ it('links logs to explorer page by default', () => {
+ const routes = generateOtherRoutes(REF, activeProject, { isPlatform: true })
+ const logsRoute = routes.find((r) => r.key === 'logs')
+ expect(logsRoute?.link).toBe(`/project/${REF}/logs/explorer`)
+ })
+
+ it('points links to building URL when project is building', () => {
+ const routes = generateOtherRoutes(REF, buildingProject, {
+ isPlatform: true,
+ showReports: true,
+ })
+ const observabilityRoute = routes.find((r) => r.key === 'observability')
+ expect(observabilityRoute?.link).toBe(`/project/${REF}`)
+ })
+
+ it('marks routes as disabled when project is not active', () => {
+ const routes = generateOtherRoutes(REF, inactiveProject, { isPlatform: true })
+ const advisorsRoute = routes.find((r) => r.key === 'advisors')
+ expect(advisorsRoute?.disabled).toBe(true)
+ })
+})
+
+describe('generateSettingsRoutes', () => {
+ it('links to general settings on platform', () => {
+ const routes = generateSettingsRoutes(REF, { isPlatform: true })
+ const settingsRoute = routes.find((r) => r.key === 'settings')
+ expect(settingsRoute?.link).toBe(`/project/${REF}/settings/general`)
+ })
+
+ it('links to log-drains settings in self-hosted mode', () => {
+ const routes = generateSettingsRoutes(REF, { isPlatform: false })
+ const settingsRoute = routes.find((r) => r.key === 'settings')
+ expect(settingsRoute?.link).toBe(`/project/${REF}/settings/log-drains`)
+ })
+
+ it('returns a link as false when ref is undefined', () => {
+ const routes = generateSettingsRoutes(undefined, { isPlatform: true })
+ const settingsRoute = routes.find((r) => r.key === 'settings')
+ expect(settingsRoute?.link).toBe(undefined)
+ })
+})
diff --git a/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.tsx
index 92d2d10145..798d65e20d 100644
--- a/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.tsx
+++ b/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.tsx
@@ -7,10 +7,43 @@ import { Auth, Database, EdgeFunctions, Realtime, SqlEditor, Storage, TableEdito
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import { Blocks, FileText, Lightbulb, List, Settings, Telescope } from 'lucide-react'
-export const generateToolRoutes = (ref?: string, project?: Project, features?: {}): Route[] => {
- const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
- const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
- const buildingUrl = `/project/${ref}`
+interface RouteContext {
+ ref?: string
+ isProjectActive: boolean
+ isProjectBuilding: boolean
+ buildingUrl: string
+}
+
+interface ProductFeatures {
+ auth?: boolean
+ edgeFunctions?: boolean
+ storage?: boolean
+ realtime?: boolean
+ authOverviewPage?: boolean
+}
+
+interface OtherFeatures {
+ isPlatform?: boolean
+ unifiedLogs?: boolean
+ showReports?: boolean
+ apiDocsSidePanel?: boolean
+}
+
+interface SettingsFeatures {
+ isPlatform?: boolean
+}
+
+function getRouteContext(ref?: string, project?: Project): RouteContext {
+ return {
+ ref,
+ isProjectActive: project?.status === PROJECT_STATUS.ACTIVE_HEALTHY,
+ isProjectBuilding: project?.status === PROJECT_STATUS.COMING_UP,
+ buildingUrl: `/project/${ref}`,
+ }
+}
+
+export const generateToolRoutes = (ref?: string, project?: Project): Route[] => {
+ const { isProjectActive, isProjectBuilding, buildingUrl } = getRouteContext(ref, project)
return [
{
@@ -34,17 +67,9 @@ export const generateToolRoutes = (ref?: string, project?: Project, features?: {
export const generateProductRoutes = (
ref?: string,
project?: Project,
- features?: {
- auth?: boolean
- edgeFunctions?: boolean
- storage?: boolean
- realtime?: boolean
- authOverviewPage?: boolean
- }
+ features?: ProductFeatures
): Route[] => {
- const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
- const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
- const buildingUrl = `/project/${ref}`
+ const { isProjectActive, isProjectBuilding, buildingUrl } = getRouteContext(ref, project)
const authEnabled = features?.auth ?? true
const edgeFunctionsEnabled = features?.edgeFunctions ?? true
@@ -122,16 +147,14 @@ export const generateProductRoutes = (
export const generateOtherRoutes = (
ref?: string,
project?: Project,
- features?: { unifiedLogs?: boolean; showReports?: boolean; apiDocsSidePanel?: boolean }
+ features?: OtherFeatures
): Route[] => {
- const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
- const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
- const buildingUrl = `/project/${ref}`
+ const { isProjectActive, isProjectBuilding, buildingUrl } = getRouteContext(ref, project)
- const { unifiedLogs, showReports, apiDocsSidePanel } = features ?? {}
- const unifiedLogsEnabled = unifiedLogs ?? false
- const reportsEnabled = showReports ?? true
- const apiDocsSidePanelEnabled = apiDocsSidePanel ?? false
+ const isPlatform = features?.isPlatform ?? IS_PLATFORM
+ const unifiedLogsEnabled = features?.unifiedLogs ?? false
+ const reportsEnabled = features?.showReports ?? true
+ const apiDocsSidePanelEnabled = features?.apiDocsSidePanel ?? false
return [
{
@@ -142,18 +165,16 @@ export const generateOtherRoutes = (
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/advisors/security`),
},
// Observability is only available on the platform, not for self-hosted/CLI
- ...(IS_PLATFORM
- ? reportsEnabled
- ? [
- {
- key: 'observability',
- label: 'Observability',
- disabled: !isProjectActive,
- icon: ,
- link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/observability`),
- },
- ]
- : []
+ ...(isPlatform && reportsEnabled
+ ? [
+ {
+ key: 'observability',
+ label: 'Observability',
+ disabled: !isProjectActive,
+ icon: ,
+ link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/observability`),
+ },
+ ]
: []),
{
key: 'logs',
@@ -185,7 +206,9 @@ export const generateOtherRoutes = (
]
}
-export const generateSettingsRoutes = (ref?: string): Route[] => {
+export const generateSettingsRoutes = (ref?: string, features?: SettingsFeatures): Route[] => {
+ const isPlatform = features?.isPlatform ?? IS_PLATFORM
+
return [
{
key: 'settings',
@@ -193,7 +216,7 @@ export const generateSettingsRoutes = (ref?: string): Route[] => {
icon: ,
link:
ref &&
- (IS_PLATFORM ? `/project/${ref}/settings/general` : `/project/${ref}/settings/log-drains`),
+ (isPlatform ? `/project/${ref}/settings/general` : `/project/${ref}/settings/log-drains`),
disabled: false,
},
]
diff --git a/apps/studio/components/layouts/ObservabilityLayout/ObservabilityMenu.tsx b/apps/studio/components/layouts/ObservabilityLayout/ObservabilityMenu.tsx
index 96cb647613..2a31a162dd 100644
--- a/apps/studio/components/layouts/ObservabilityLayout/ObservabilityMenu.tsx
+++ b/apps/studio/components/layouts/ObservabilityLayout/ObservabilityMenu.tsx
@@ -20,9 +20,10 @@ import { cn, Menu } from 'ui'
import { InnerSideBarEmptyPanel } from 'ui-patterns'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
-import { useSupamonitorStatus } from '@/components/interfaces/QueryPerformance/hooks/useSupamonitorStatus'
+import { generateObservabilityMenuItems } from './ObservabilityMenu.utils'
import { ObservabilityMenuItem } from './ObservabilityMenuItem'
+import { useSupamonitorStatus } from '@/components/interfaces/QueryPerformance/hooks/useSupamonitorStatus'
const ObservabilityMenu = () => {
const router = useRouter()
@@ -124,87 +125,14 @@ const ObservabilityMenu = () => {
const reportMenuItems = getReportMenuItems()
- const menuItems = [
- {
- title: 'GENERAL',
- key: 'general-section',
- items: [
- ...(showOverview
- ? [
- {
- name: 'Overview',
- key: 'observability',
- url: `/project/${ref}/observability${preservedQueryParams}`,
- },
- ]
- : []),
- ...(isSupamonitorEnabled
- ? [
- {
- name: 'Query Insights',
- key: 'query-insights',
- url: `/project/${ref}/observability/query-insights${preservedQueryParams}`,
- },
- ]
- : [
- {
- name: 'Query Performance',
- key: 'query-performance',
- url: `/project/${ref}/observability/query-performance${preservedQueryParams}`,
- },
- ]),
- ...(IS_PLATFORM
- ? [
- {
- name: 'API Gateway',
- key: 'api-overview',
- url: `/project/${ref}/observability/api-overview${preservedQueryParams}`,
- },
- ]
- : []),
- ],
- },
- {
- title: 'PRODUCT',
- key: 'product-section',
- items: [
- {
- name: 'Database',
- key: 'database',
- url: `/project/${ref}/observability/database${preservedQueryParams}`,
- },
- {
- name: 'Data API',
- key: 'postgrest',
- url: `/project/${ref}/observability/postgrest${preservedQueryParams}`,
- },
- {
- name: 'Auth',
- key: 'auth',
- url: `/project/${ref}/observability/auth${preservedQueryParams}`,
- },
- {
- name: 'Edge Functions',
- key: 'edge-functions',
- url: `/project/${ref}/observability/edge-functions${preservedQueryParams}`,
- },
- ...(storageSupported
- ? [
- {
- name: 'Storage',
- key: 'storage',
- url: `/project/${ref}/observability/storage${preservedQueryParams}`,
- },
- ]
- : []),
- {
- name: 'Realtime',
- key: 'realtime',
- url: `/project/${ref}/observability/realtime${preservedQueryParams}`,
- },
- ],
- },
- ]
+ const menuItems = generateObservabilityMenuItems({
+ ref,
+ preservedQueryParams,
+ showOverview,
+ isSupamonitorEnabled,
+ storageSupported,
+ isPlatform: IS_PLATFORM,
+ })
return (