diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx
index f9291d4db11..3e0c87901ec 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useFlag, useParams } from 'common'
+import { useParams } from 'common'
import { CreateAnalyticsBucketSheet } from 'components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketSheet'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
@@ -35,6 +35,10 @@ import {
import { Button, DialogSectionSeparator, Form_Shadcn_, SheetFooter, SheetSection } from 'ui'
import * as z from 'zod'
+import {
+ useIsETLBigQueryPrivateAlpha,
+ useIsETLIcebergPrivateAlpha,
+} from '../../useIsETLPrivateAlpha'
import { DestinationType } from '../DestinationPanel.types'
import { AdvancedSettings } from './AdvancedSettings'
import { CREATE_NEW_NAMESPACE } from './DestinationForm.constants'
@@ -75,8 +79,8 @@ export const DestinationForm = ({
const { ref: projectRef } = useParams()
const { setRequestStatus } = usePipelineRequestStatus()
- const etlEnableBigQuery = useFlag('etlEnableBigQuery')
- const etlEnableIceberg = useFlag('etlEnableIceberg')
+ const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha()
+ const etlEnableIceberg = useIsETLIcebergPrivateAlpha()
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
const [isFormInteracting, setIsFormInteracting] = useState(false)
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx
index 8c2d69d6437..68e51584c9b 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx
@@ -7,6 +7,7 @@ import { useEffect } from 'react'
import { toast } from 'sonner'
import {
Button,
+ cn,
DialogSectionSeparator,
Sheet,
SheetContent,
@@ -14,7 +15,6 @@ import {
SheetHeader,
SheetSection,
SheetTitle,
- cn,
} from 'ui'
import { EnableReplicationCallout } from '../EnableReplicationCallout'
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx
index af2328c95e0..58d5350bc95 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx
@@ -1,18 +1,17 @@
import { useFlag } from 'common'
import { AnalyticsBucket, BigQuery, Database } from 'icons'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
-import { Badge, RadioGroupStacked, RadioGroupStackedItem, cn } from 'ui'
+import { Badge, cn, RadioGroupStacked, RadioGroupStackedItem } from 'ui'
import { useDestinationInformation } from '../useDestinationInformation'
-import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha'
+import { useIsETLBigQueryPrivateAlpha, useIsETLIcebergPrivateAlpha } from '../useIsETLPrivateAlpha'
import { DestinationType } from './DestinationPanel.types'
import { InlineLink } from '@/components/ui/InlineLink'
export const DestinationTypeSelection = () => {
- const enablePgReplicate = useIsETLPrivateAlpha()
const unifiedReplication = useFlag('unifiedReplication')
- const etlEnableBigQuery = useFlag('etlEnableBigQuery')
- const etlEnableIceberg = useFlag('etlEnableIceberg')
+ const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha()
+ const etlEnableIceberg = useIsETLIcebergPrivateAlpha()
const numberOfTypes = [unifiedReplication, etlEnableBigQuery, etlEnableIceberg].filter(
Boolean
@@ -52,8 +51,8 @@ export const DestinationTypeSelection = () => {
value={destinationType}
onValueChange={(value) => setDestinationType(value as DestinationType)}
className={cn(
- 'grid [&>button>div]:py-4',
- numberOfTypes === 3 ? 'grid-cols-3' : numberOfTypes === 2 ? 'grid-cols-2' : 'grid-cols-1',
+ 'grid [&>button>div]:py-4 grid-cols-3',
+ numberOfTypes === 3 && !editMode ? 'grid-cols-3' : 'grid-cols-2',
'[&>button:first-of-type]:rounded-none [&>button:last-of-type]:rounded-none',
'[&>button:first-of-type]:!rounded-l-lg [&>button:last-of-type]:!rounded-r-lg'
)}
@@ -121,7 +120,7 @@ export const DestinationTypeSelection = () => {
)}
- {destinationType !== 'Read Replica' && enablePgReplicate && (
+ {destinationType !== 'Read Replica' && (
Replication is in alpha. Expect rapid changes and possible breaking updates.{' '}
diff --git a/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts b/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts
index 04486c97f74..351d8095e56 100644
--- a/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts
+++ b/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts
@@ -2,18 +2,37 @@ import { useFlag } from 'common'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
/**
- * Organization level opt in for ETL private alpha
+ * Organization level opt in for ETL private alpha, there's 2 flags we're using which controls
+ * the individual destination types. Access to the ETL UI (`useIsETLPrivateAlpha`) will just
+ * check if the org has access to at least one of the destination types
*/
-export const useIsETLPrivateAlpha = () => {
+const useIsCurrentOrgInFlagList = (flag: string) => {
+ const flagValue = useFlag(flag)
const { data: organization } = useSelectedOrganizationQuery()
- const etlPrivateAlpha = useFlag('etlPrivateAlpha')
- const privateAlphaOrgSlugs =
- typeof etlPrivateAlpha === 'string'
- ? (etlPrivateAlpha as string).split(',').map((x) => x.trim())
+ const allowedOrgSlugs =
+ typeof flagValue === 'string'
+ ? (flagValue as string).split(',').map((x: string) => x.trim())
: []
- const etlShowForAllProjects = useFlag('etlPrivateAlphaOverride')
+ // [Joshen] Override for to enable for all organizations by setting the flag value as `all`
+ if (allowedOrgSlugs.includes('all')) return true
- return etlShowForAllProjects || privateAlphaOrgSlugs.includes(organization?.slug ?? '')
+ // [Joshen] Otherwise fallback to checking against org slug
+ return allowedOrgSlugs.includes(organization?.slug ?? '')
+}
+
+export const useIsETLBigQueryPrivateAlpha = () => {
+ return useIsCurrentOrgInFlagList('etlEnableBigQueryPrivateAlpha')
+}
+
+export const useIsETLIcebergPrivateAlpha = () => {
+ return useIsCurrentOrgInFlagList('etlEnableIcebergPrivateAlpha')
+}
+
+export const useIsETLPrivateAlpha = () => {
+ const hasAccessToETLBigQuery = useIsCurrentOrgInFlagList('etlEnableBigQueryPrivateAlpha')
+ const hasAccessToETLIceberg = useIsCurrentOrgInFlagList('etlEnableIcebergPrivateAlpha')
+
+ return hasAccessToETLBigQuery || hasAccessToETLIceberg
}
diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx
index a7967289937..ae652ef847a 100644
--- a/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx
+++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx
@@ -1,60 +1,21 @@
+import { ProductMenu } from 'components/ui/ProductMenu'
+import { withAuth } from 'hooks/misc/withAuth'
import { useRouter } from 'next/router'
import { PropsWithChildren } from 'react'
-import { useIsColumnLevelPrivilegesEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
-import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication/useIsETLPrivateAlpha'
-import { ProductMenu } from 'components/ui/ProductMenu'
-import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
-import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
-import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
-import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
-import { withAuth } from 'hooks/misc/withAuth'
import { ProjectLayout } from '../ProjectLayout'
-import { generateDatabaseMenu } from './DatabaseMenu.utils'
+import { useGenerateDatabaseMenu } from './DatabaseMenu.utils'
export interface DatabaseLayoutProps {
title?: string
}
const DatabaseProductMenu = () => {
- const { data: project } = useSelectedProjectQuery()
-
const router = useRouter()
const page = router.pathname.split('/')[4]
+ const menu = useGenerateDatabaseMenu()
- const { data } = useDatabaseExtensionsQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref })
-
- const pgNetExtensionExists = (data ?? []).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'])
-
- return (
- <>
-
- >
- )
+ return
}
const DatabaseLayout = ({ children }: PropsWithChildren) => {
diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx
index 6939c2d6a05..e8a6a4bb1c0 100644
--- a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx
+++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx
@@ -1,163 +1,104 @@
+import { useParams } from 'common'
+import type {
+ ProductMenuGroup,
+ ProductMenuGroupItem,
+} from 'components/ui/ProductMenu/ProductMenu.types'
+import { IS_PLATFORM } from 'lib/constants'
import { ArrowUpRight } from 'lucide-react'
-import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
-import type { Project } from 'data/projects/project-detail-query'
-import { IS_PLATFORM } from 'lib/constants'
+import { useIsColumnLevelPrivilegesEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
+import { useIsETLPrivateAlpha } from '@/components/interfaces/Database/Replication/useIsETLPrivateAlpha'
+import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
+import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
+import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+
+const ExternalLinkIcon =
+
+export const useGenerateDatabaseMenu = (): ProductMenuGroup[] => {
+ const { ref } = useParams()
+ const { data: project } = useSelectedProjectQuery()
-export const generateDatabaseMenu = (
- project?: Project,
- flags?: {
- pgNetExtensionExists: boolean
- pitrEnabled: boolean
- columnLevelPrivileges: boolean
- showPgReplicate: boolean
- enablePgReplicate: boolean
- showRoles: boolean
- showWrappers: boolean
- }
-): ProductMenuGroup[] => {
- const ref = project?.ref ?? 'default'
const {
- pgNetExtensionExists,
- pitrEnabled,
- columnLevelPrivileges,
- showPgReplicate,
- enablePgReplicate,
- showRoles,
- showWrappers,
- } = flags || {}
+ databaseReplication: showPgReplicate,
+ databaseRoles: showRoles,
+ integrationsWrappers: showWrappers,
+ } = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers'])
+
+ const { data } = useDatabaseExtensionsQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ })
+ const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref })
+
+ const pgNetExtensionExists = (data ?? []).some((ext) => ext.name === 'pg_net')
+ const pitrEnabled = addons?.selected_addons.some((addon) => addon.type === 'pitr') ?? false
+ const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
+ const enablePgReplicate = useIsETLPrivateAlpha()
+
+ const getDatabaseURL = (path: string) => `/project/${ref}/database/${path}`
return [
{
title: 'Database Management',
items: [
- {
- name: 'Schema Visualizer',
- key: 'schemas',
- url: `/project/${ref}/database/schemas`,
- items: [],
- },
- { name: 'Tables', key: 'tables', url: `/project/${ref}/database/tables`, items: [] },
- {
- name: 'Functions',
- key: 'functions',
- url: `/project/${ref}/database/functions`,
- items: [],
- },
- {
- name: 'Triggers',
- key: 'triggers',
- url: `/project/${ref}/database/triggers/data`,
- items: [],
- },
- {
- name: 'Enumerated Types',
- key: 'types',
- url: `/project/${ref}/database/types`,
-
- items: [],
- },
- {
- name: 'Extensions',
- key: 'extensions',
- url: `/project/${ref}/database/extensions`,
- items: [],
- },
- {
- name: 'Indexes',
- key: 'indexes',
- url: `/project/${ref}/database/indexes`,
- items: [],
- },
- {
- name: 'Publications',
- key: 'publications',
- url: `/project/${ref}/database/publications`,
- items: [],
- },
+ { name: 'Schema Visualizer', key: 'schemas', url: getDatabaseURL('schemas') },
+ { name: 'Tables', key: 'tables', url: getDatabaseURL('tables') },
+ { name: 'Functions', key: 'functions', url: getDatabaseURL('functions') },
+ { name: 'Triggers', key: 'triggers', url: getDatabaseURL('triggers/data') },
+ { name: 'Enumerated Types', key: 'types', url: getDatabaseURL('types') },
+ { name: 'Extensions', key: 'extensions', url: getDatabaseURL('extensions') },
+ { name: 'Indexes', key: 'indexes', url: getDatabaseURL('indexes') },
+ { name: 'Publications', key: 'publications', url: getDatabaseURL('publications') },
],
},
{
title: 'Configuration',
items: [
- ...(showRoles
- ? [{ name: 'Roles', key: 'roles', url: `/project/${ref}/database/roles`, items: [] }]
- : []),
- ...(columnLevelPrivileges
- ? [
- {
- name: 'Column Privileges',
- key: 'column-privileges',
- url: `/project/${ref}/database/column-privileges`,
- items: [],
- },
- ]
- : []),
+ showRoles && { name: 'Roles', key: 'roles', url: getDatabaseURL('roles') },
+ columnLevelPrivileges && {
+ name: 'Column Privileges',
+ key: 'column-privileges',
+ url: getDatabaseURL('column-privileges'),
+ },
{
name: 'Policies',
key: 'policies',
url: `/project/${ref}/auth/policies`,
- rightIcon: ,
- items: [],
+ rightIcon: ExternalLinkIcon,
},
- { name: 'Settings', key: 'settings', url: `/project/${ref}/database/settings`, items: [] },
- ],
+ { name: 'Settings', key: 'settings', url: getDatabaseURL('settings') },
+ ].filter(Boolean) as ProductMenuGroupItem[],
},
{
title: 'Platform',
items: [
- ...(IS_PLATFORM && showPgReplicate
- ? [
- {
- name: 'Replication',
- key: 'replication',
- url: `/project/${ref}/database/replication`,
- label: enablePgReplicate ? 'New' : undefined,
- items: [],
- },
- ]
- : []),
- ...(IS_PLATFORM
- ? [
- {
- name: 'Backups',
- key: 'backups',
- url: pitrEnabled
- ? `/project/${ref}/database/backups/pitr`
- : `/project/${ref}/database/backups/scheduled`,
- items: [],
- },
- ]
- : []),
- {
- name: 'Migrations',
- key: 'migrations',
- url: `/project/${ref}/database/migrations`,
- items: [],
+ IS_PLATFORM &&
+ showPgReplicate && {
+ name: 'Replication',
+ key: 'replication',
+ url: getDatabaseURL('replication'),
+ label: enablePgReplicate ? 'New' : undefined,
+ },
+ IS_PLATFORM && {
+ name: 'Backups',
+ key: 'backups',
+ url: pitrEnabled ? getDatabaseURL('backups/pitr') : getDatabaseURL('backups/scheduled'),
},
- ...(showWrappers
- ? [
- {
- name: 'Wrappers',
- key: 'wrappers',
- url: `/project/${ref}/integrations?category=wrapper`,
- rightIcon: ,
- items: [],
- },
- ]
- : []),
- ...(!!pgNetExtensionExists
- ? [
- {
- name: 'Webhooks',
- key: 'hooks',
- url: `/project/${ref}/integrations/webhooks/overview`,
- rightIcon: ,
- items: [],
- },
- ]
- : []),
- ],
+ { name: 'Migrations', key: 'migrations', url: getDatabaseURL('migrations') },
+ showWrappers && {
+ name: 'Wrappers',
+ key: 'wrappers',
+ url: `/project/${ref}/integrations?category=wrapper`,
+ rightIcon: ExternalLinkIcon,
+ },
+ pgNetExtensionExists && {
+ name: 'Webhooks',
+ key: 'hooks',
+ url: `/project/${ref}/integrations/webhooks/overview`,
+ rightIcon: ExternalLinkIcon,
+ },
+ ].filter(Boolean) as ProductMenuGroupItem[],
},
{
title: 'Tools',
@@ -166,22 +107,19 @@ export const generateDatabaseMenu = (
name: 'Security Advisor',
key: 'security-advisor',
url: `/project/${ref}/advisors/security`,
- rightIcon: ,
- items: [],
+ rightIcon: ExternalLinkIcon,
},
{
name: 'Performance Advisor',
key: 'performance-advisor',
url: `/project/${ref}/advisors/performance`,
- rightIcon: ,
- items: [],
+ rightIcon: ExternalLinkIcon,
},
{
name: 'Query Performance',
key: 'query-performance',
url: `/project/${ref}/observability/query-performance`,
- rightIcon: ,
- items: [],
+ rightIcon: ExternalLinkIcon,
},
],
},
diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
index 995e6915d87..30bebf679ab 100644
--- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
@@ -1,6 +1,4 @@
import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar'
-import { generateAuthMenu } from 'components/layouts/AuthLayout/AuthLayout.utils'
-import { generateDatabaseMenu } from 'components/layouts/DatabaseLayout/DatabaseMenu.utils'
import { generateSettingsMenu } from 'components/layouts/ProjectSettingsLayout/SettingsMenu.utils'
import type { Route } from 'components/ui/ui.types'
import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor'
@@ -54,9 +52,6 @@ export const generateProductRoutes = (
const realtimeEnabled = features?.realtime ?? true
const authOverviewPageEnabled = features?.authOverviewPage ?? false
- const databaseMenu = generateDatabaseMenu(project)
- const authMenu = generateAuthMenu(ref as string)
-
return [
{
key: 'database',
@@ -70,7 +65,6 @@ export const generateProductRoutes = (
: isProjectActive
? `/project/${ref}/database/schemas`
: `/project/${ref}/database/backups/scheduled`),
- items: databaseMenu,
},
...(authEnabled
? [
@@ -86,7 +80,6 @@ export const generateProductRoutes = (
: authOverviewPageEnabled
? `/project/${ref}/auth/overview`
: `/project/${ref}/auth/users`),
- items: authMenu,
},
]
: []),