mirror of
https://github.com/supabase/supabase.git
synced 2026-09-09 11:30:17 +08:00
## Context As per PR title - sets up incremental default opt in for the Database Connections feature preview Database Connections preview banner should still only show up if it's never been dismissed before, but the CTA's changed to "Explore" rather than "Enable" if the user's default opted in Related discussion here: https://github.com/orgs/supabase/discussions/48639 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Database Connections preview is now enabled by default. * Added clearer handling for preview state and initialization. * Banner actions open Database Connections when enabled, or the feature preview when disabled. * **Bug Fixes** * Improved banner and menu visibility while preview settings initialize. * Preserved banner dismissal behavior after a previous preference change. * Improved navigation consistency across Database Connections entry points. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
import { useFlag, useParams } from 'common'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
useGenerateCustomReportsMenu,
|
|
useGenerateObservabilityMenu,
|
|
} from './ObservabilityMenu.utils'
|
|
import { useSupamonitorStatus } from '@/components/interfaces/QueryPerformance/hooks/useSupamonitorStatus'
|
|
import { useContentQuery } from '@/data/content/content-query'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
import { customRenderHook as renderHook } from '@/tests/lib/custom-render'
|
|
import { routerMock } from '@/tests/lib/route-mock'
|
|
|
|
const { REF, mockIsPlatform } = vi.hoisted(() => ({
|
|
REF: 'project-ref',
|
|
mockIsPlatform: { value: true },
|
|
}))
|
|
|
|
vi.mock('@/lib/constants', async () => {
|
|
const actual = await vi.importActual<Record<string, unknown>>('@/lib/constants')
|
|
return {
|
|
...actual,
|
|
get IS_PLATFORM() {
|
|
return mockIsPlatform.value
|
|
},
|
|
}
|
|
})
|
|
|
|
vi.mock('common', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('common')>()
|
|
return {
|
|
...actual,
|
|
useFlag: vi.fn().mockReturnValue(false),
|
|
useParams: vi.fn().mockReturnValue({ ref: REF }),
|
|
}
|
|
})
|
|
|
|
vi.mock('@/components/interfaces/QueryPerformance/hooks/useSupamonitorStatus', () => ({
|
|
useSupamonitorStatus: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({
|
|
useIsFeatureEnabled: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/data/content/content-query', () => ({
|
|
useContentQuery: vi.fn(),
|
|
}))
|
|
|
|
describe('useGenerateObservabilityMenu', () => {
|
|
beforeEach(() => {
|
|
mockIsPlatform.value = true
|
|
routerMock.setCurrentUrl(`/project/${REF}/observability`)
|
|
vi.mocked(useFlag).mockReturnValue(false)
|
|
vi.mocked(useParams).mockReturnValue({ ref: REF })
|
|
vi.mocked(useSupamonitorStatus).mockReturnValue({
|
|
isSupamonitorEnabled: false,
|
|
isLoading: false,
|
|
})
|
|
vi.mocked(useIsFeatureEnabled).mockReturnValue(true)
|
|
})
|
|
|
|
it('always includes the GENERAL section', () => {
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
|
|
expect(result.current.some((section) => section.title === 'GENERAL')).toBe(true)
|
|
})
|
|
|
|
it('includes Overview when the observabilityOverview flag is enabled', () => {
|
|
vi.mocked(useFlag).mockReturnValue(true)
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const general = result.current.find((section) => section.title === 'GENERAL')
|
|
|
|
expect(general?.items.some((item) => item.key === 'observability')).toBe(true)
|
|
})
|
|
|
|
it('excludes Overview when the observabilityOverview flag is disabled', () => {
|
|
vi.mocked(useFlag).mockReturnValue(false)
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const general = result.current.find((section) => section.title === 'GENERAL')
|
|
|
|
expect(general?.items.some((item) => item.key === 'observability')).toBe(false)
|
|
})
|
|
|
|
it('shows Query Performance when supamonitor is disabled', () => {
|
|
vi.mocked(useSupamonitorStatus).mockReturnValue({
|
|
isSupamonitorEnabled: false,
|
|
isLoading: false,
|
|
})
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const general = result.current.find((section) => section.title === 'GENERAL')
|
|
|
|
expect(general?.items.some((item) => item.key === 'query-performance')).toBe(true)
|
|
expect(general?.items.some((item) => item.key === 'query-insights')).toBe(false)
|
|
})
|
|
|
|
it('shows Query Insights when supamonitor is enabled', () => {
|
|
vi.mocked(useSupamonitorStatus).mockReturnValue({
|
|
isSupamonitorEnabled: true,
|
|
isLoading: false,
|
|
})
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const general = result.current.find((section) => section.title === 'GENERAL')
|
|
|
|
expect(general?.items.some((item) => item.key === 'query-insights')).toBe(true)
|
|
expect(general?.items.some((item) => item.key === 'query-performance')).toBe(false)
|
|
})
|
|
|
|
it('includes API Gateway and the PRODUCT section on platform', () => {
|
|
mockIsPlatform.value = true
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const general = result.current.find((section) => section.title === 'GENERAL')
|
|
|
|
expect(general?.items.some((item) => item.key === 'api-overview')).toBe(true)
|
|
expect(result.current.some((section) => section.title === 'PRODUCT')).toBe(true)
|
|
})
|
|
|
|
it('excludes API Gateway and the PRODUCT section in self-hosted mode', () => {
|
|
mockIsPlatform.value = false
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const general = result.current.find((section) => section.title === 'GENERAL')
|
|
|
|
expect(general?.items.some((item) => item.key === 'api-overview')).toBe(false)
|
|
expect(result.current.some((section) => section.title === 'PRODUCT')).toBe(false)
|
|
expect(result.current.length).toBe(1)
|
|
})
|
|
|
|
it('includes Storage in the PRODUCT section when project_storage:all is enabled', () => {
|
|
vi.mocked(useIsFeatureEnabled).mockReturnValue(true)
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const product = result.current.find((section) => section.title === 'PRODUCT')
|
|
|
|
expect(product?.items.some((item) => item.key === 'storage')).toBe(true)
|
|
expect(useIsFeatureEnabled).toHaveBeenCalledWith('project_storage:all')
|
|
})
|
|
|
|
it('excludes Storage from the PRODUCT section when project_storage:all is disabled', () => {
|
|
vi.mocked(useIsFeatureEnabled).mockReturnValue(false)
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const product = result.current.find((section) => section.title === 'PRODUCT')
|
|
|
|
expect(product?.items.some((item) => item.key === 'storage')).toBe(false)
|
|
})
|
|
|
|
it('always includes Database, Data API, Auth, Edge Functions, and Realtime in the PRODUCT section', () => {
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const product = result.current.find((section) => section.title === 'PRODUCT')
|
|
|
|
expect(product?.items.map((item) => item.key)).toEqual(
|
|
expect.arrayContaining(['database', 'postgrest', 'auth', 'edge-functions', 'realtime'])
|
|
)
|
|
})
|
|
|
|
it('builds menu item URLs using the project ref', () => {
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const product = result.current.find((section) => section.title === 'PRODUCT')
|
|
const database = product?.items.find((item) => item.key === 'database')
|
|
|
|
expect(database?.url).toBe(`/project/${REF}/observability/database`)
|
|
})
|
|
|
|
it('handles an undefined project ref', () => {
|
|
vi.mocked(useParams).mockReturnValue({ ref: undefined })
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const product = result.current.find((section) => section.title === 'PRODUCT')
|
|
const database = product?.items.find((item) => item.key === 'database')
|
|
|
|
expect(database?.url).toBe('/project/undefined/observability/database')
|
|
})
|
|
|
|
it('preserves date range and helper query params across menu item URLs', () => {
|
|
routerMock.setCurrentUrl(
|
|
`/project/${REF}/observability?its=2024-01-01&ite=2024-01-31&isHelper=true&helperText=hello`
|
|
)
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const product = result.current.find((section) => section.title === 'PRODUCT')
|
|
const database = product?.items.find((item) => item.key === 'database')
|
|
|
|
expect(database?.url).toBe(
|
|
`/project/${REF}/observability/database?its=2024-01-01&ite=2024-01-31&isHelper=true&helperText=hello`
|
|
)
|
|
})
|
|
|
|
it('ignores query param values that are not plain strings (e.g. duplicated keys)', () => {
|
|
routerMock.setCurrentUrl(`/project/${REF}/observability?its=2024-01-01&its=2024-02-01`)
|
|
|
|
const { result } = renderHook(() => useGenerateObservabilityMenu())
|
|
const product = result.current.find((section) => section.title === 'PRODUCT')
|
|
const database = product?.items.find((item) => item.key === 'database')
|
|
|
|
expect(database?.url).toBe(`/project/${REF}/observability/database`)
|
|
})
|
|
})
|
|
|
|
describe('useGenerateCustomReportsMenu', () => {
|
|
const buildReport = (overrides: Record<string, unknown> = {}) => ({
|
|
id: 'report-1',
|
|
name: 'Report',
|
|
description: '',
|
|
type: 'report',
|
|
content: {},
|
|
...overrides,
|
|
})
|
|
|
|
beforeEach(() => {
|
|
routerMock.setCurrentUrl(`/project/${REF}/observability`)
|
|
})
|
|
|
|
it('returns an empty array and reports loading while content is pending', () => {
|
|
vi.mocked(useContentQuery).mockReturnValue({ data: undefined, isPending: true } as any)
|
|
|
|
const { result } = renderHook(() => useGenerateCustomReportsMenu())
|
|
|
|
expect(result.current.isLoading).toBe(true)
|
|
expect(result.current.data).toEqual([])
|
|
})
|
|
|
|
it('returns an empty array when there is no content', () => {
|
|
vi.mocked(useContentQuery).mockReturnValue({ data: undefined, isPending: false } as any)
|
|
|
|
const { result } = renderHook(() => useGenerateCustomReportsMenu())
|
|
|
|
expect(result.current.isLoading).toBe(false)
|
|
expect(result.current.data).toEqual([])
|
|
})
|
|
|
|
it('filters out non-report content', () => {
|
|
vi.mocked(useContentQuery).mockReturnValue({
|
|
data: {
|
|
cursor: null,
|
|
content: [
|
|
buildReport({ id: 'r1', name: 'A Report' }),
|
|
{ id: 'sql-1', name: 'A snippet', description: '', type: 'sql', content: {} },
|
|
],
|
|
},
|
|
isPending: false,
|
|
} as any)
|
|
|
|
const { result } = renderHook(() => useGenerateCustomReportsMenu())
|
|
|
|
expect(result.current.data).toHaveLength(1)
|
|
expect(result.current.data[0].id).toBe('r1')
|
|
})
|
|
|
|
it('sorts reports alphabetically by name', () => {
|
|
vi.mocked(useContentQuery).mockReturnValue({
|
|
data: {
|
|
cursor: null,
|
|
content: [
|
|
buildReport({ id: 'r2', name: 'Zebra' }),
|
|
buildReport({ id: 'r1', name: 'Apple' }),
|
|
],
|
|
},
|
|
isPending: false,
|
|
} as any)
|
|
|
|
const { result } = renderHook(() => useGenerateCustomReportsMenu())
|
|
|
|
expect(result.current.data.map((item) => item.name)).toEqual(['Apple', 'Zebra'])
|
|
})
|
|
|
|
it('builds report URLs preserving query params and defaults a missing description to an empty string', () => {
|
|
routerMock.setCurrentUrl(`/project/${REF}/observability?its=2024-01-01`)
|
|
vi.mocked(useContentQuery).mockReturnValue({
|
|
data: {
|
|
cursor: null,
|
|
content: [buildReport({ id: 'r1', name: 'Report', description: undefined })],
|
|
},
|
|
isPending: false,
|
|
} as any)
|
|
|
|
const { result } = renderHook(() => useGenerateCustomReportsMenu())
|
|
|
|
expect(result.current.data[0].url).toBe(`/project/${REF}/observability/r1?its=2024-01-01`)
|
|
expect(result.current.data[0].description).toBe('')
|
|
})
|
|
|
|
it('falls back to an index-based key when the report id is missing', () => {
|
|
vi.mocked(useContentQuery).mockReturnValue({
|
|
data: {
|
|
cursor: null,
|
|
content: [buildReport({ id: undefined, name: 'Report' })],
|
|
},
|
|
isPending: false,
|
|
} as any)
|
|
|
|
const { result } = renderHook(() => useGenerateCustomReportsMenu())
|
|
|
|
expect(result.current.data[0].key).toBe('0-report')
|
|
})
|
|
|
|
it('marks every report item as having dropdown actions and carries the raw report', () => {
|
|
const report = buildReport({ id: 'r1', name: 'Report' })
|
|
vi.mocked(useContentQuery).mockReturnValue({
|
|
data: { cursor: null, content: [report] },
|
|
isPending: false,
|
|
} as any)
|
|
|
|
const { result } = renderHook(() => useGenerateCustomReportsMenu())
|
|
|
|
expect(result.current.data[0].hasDropdownActions).toBe(true)
|
|
expect(result.current.data[0].report).toEqual(report)
|
|
})
|
|
})
|