diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx
index 9000583ef7f..63f703d7325 100644
--- a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx
+++ b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx
@@ -306,7 +306,7 @@ export const DatabaseConnectionString = () => {
align="start"
buttonProps={{
size: 'small',
- className: 'w-full justify-between pr-2.5 [&_svg]:h-4',
+ className: 'w-full pr-2.5 [&_svg]:h-4',
}}
className="w-full md:w-auto [&>span]:w-1/2 [&>span]:md:w-auto"
onSelectId={handleDatabaseChange}
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/ReadReplicaForm/ReadReplicaEligibilityWarnings.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/ReadReplicaForm/ReadReplicaEligibilityWarnings.tsx
index 62da219a858..0417a681631 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/ReadReplicaForm/ReadReplicaEligibilityWarnings.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/ReadReplicaForm/ReadReplicaEligibilityWarnings.tsx
@@ -1,8 +1,4 @@
import { SupportCategories } from '@supabase/shared-types/out/constants'
-import Link from 'next/link'
-import { useEffect, useState } from 'react'
-import { toast } from 'sonner'
-
import { useParams } from 'common'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { DocsButton } from 'components/ui/DocsButton'
@@ -13,8 +9,12 @@ import { MAX_REPLICAS_ABOVE_XL, MAX_REPLICAS_BELOW_XL } from 'data/read-replicas
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
+import Link from 'next/link'
+import { useEffect, useState } from 'react'
+import { toast } from 'sonner'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns'
+
import { useCheckEligibilityDeployReplica } from './useCheckEligibilityDeployReplica'
export const ReadReplicaEligibilityWarnings = () => {
@@ -138,9 +138,7 @@ export const ReadReplicaEligibilityWarnings = () => {
addon="computeSize"
source="read-replicas"
featureProposition="deploy Read Replicas"
- >
- Change compute size
-
+ />
diff --git a/apps/studio/components/interfaces/Settings/General/DashboardPreferences.tsx b/apps/studio/components/interfaces/Settings/General/DashboardPreferences.tsx
new file mode 100644
index 00000000000..050d49db8ab
--- /dev/null
+++ b/apps/studio/components/interfaces/Settings/General/DashboardPreferences.tsx
@@ -0,0 +1,194 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import { LOCAL_STORAGE_KEYS, useParams } from 'common'
+import { HelpCircle } from 'lucide-react'
+import { useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import {
+ Button,
+ Card,
+ CardContent,
+ CardFooter,
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogSection,
+ DialogSectionSeparator,
+ DialogTitle,
+ DialogTrigger,
+ Form_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+} from 'ui'
+import { Admonition } from 'ui-patterns'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
+import ShimmeringLoader, { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+import * as z from 'zod'
+
+import { DatabaseSelector } from '@/components/ui/DatabaseSelector'
+import { InlineLink } from '@/components/ui/InlineLink'
+import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
+import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
+
+const formSchema = z.object({
+ defaultDatabase: z.string().optional(),
+})
+
+export type DashboardPreference = z.infer
+
+const DEFAULT_PREFERENCE: DashboardPreference = {
+ defaultDatabase: undefined,
+}
+
+/**
+ * [Joshen] JFYI am not convinced about the UX of this, will iterate over time
+ * and only release to public when we're satisfied with how it behaves
+ * - Where should "Dashboard preferences" live
+ * - Preferences currently only apply to the user via local storage until we have middleware support that will persist the setting on the project for all users
+ * - Should selecting which database to run queries for dashboard be an option for users to configure (or for us to just default to)
+ * - I'd love for this to work seamlessly, but main concern atm is latency which is region dependent
+ * - Also, current database logic only applies to Table Editor atm, will need to extend it further to other pages
+ */
+
+export const DashboardPreferences = () => {
+ const { ref: projectRef } = useParams()
+
+ const [dashboardPreferences, setDashboardPreferences, { isLoading }] =
+ useLocalStorageQuery(
+ LOCAL_STORAGE_KEYS.DASHBOARD_PREFERENCES(projectRef ?? '_'),
+ DEFAULT_PREFERENCE
+ )
+
+ const { isPending } = useReadReplicasQuery({ projectRef })
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: dashboardPreferences,
+ values: dashboardPreferences,
+ mode: 'onSubmit',
+ reValidateMode: 'onBlur',
+ })
+
+ const onSubmit = async (values: DashboardPreference) => {
+ if (!projectRef) return console.error('Ref is required')
+ setDashboardPreferences(values)
+ form.reset(values)
+ toast.success('Successfully saved dashboard preferences!')
+ }
+
+ return (
+
+
+ {/* [Joshen] Ideally we're able to persist this for all users in the project, but will need support in our middleware */}
+
+
+ {isLoading ? (
+
+
+
+
+
+ ) : (
+
+
+ }
+ className="[&>div]:md:w-1/2"
+ >
+ {isPending ? (
+
+ ) : (
+
+ {/* [Joshen] Need to disable unhealthy replicas */}
+
+ field.onChange(id === projectRef ? undefined : id)
+ }
+ />
+
+ )}
+
+ )}
+ />
+
+
+ {form.formState.isDirty && (
+
+ )}
+
+
+
+
+
+ )}
+
+
+ )
+}
+
+const DashboardQueriesDialog = () => {
+ const { ref } = useParams()
+ return (
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Settings/General/General.tsx b/apps/studio/components/interfaces/Settings/General/General.tsx
index 4544eecd26a..03c373f2a3b 100644
--- a/apps/studio/components/interfaces/Settings/General/General.tsx
+++ b/apps/studio/components/interfaces/Settings/General/General.tsx
@@ -1,28 +1,22 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { BarChart2 } from 'lucide-react'
+import { useProjectUpdateMutation } from 'data/projects/project-update-mutation'
+import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
+import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import Link from 'next/link'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
-import * as z from 'zod'
-
-import { useProjectUpdateMutation } from 'data/projects/project-update-mutation'
-import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
-import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
-import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import {
+ Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
- Alert_Shadcn_,
Button,
Card,
CardContent,
CardFooter,
+ Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
- FormMessage_Shadcn_,
- Form_Shadcn_,
Input_Shadcn_,
WarningIcon,
} from 'ui'
@@ -31,25 +25,17 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
PageSection,
PageSectionContent,
- PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
-import PauseProjectButton from './Infrastructure/PauseProjectButton'
-import RestartServerButton from './Infrastructure/RestartServerButton'
+import * as z from 'zod'
export const General = () => {
const { data: project } = useSelectedProjectQuery()
- const { data: organization } = useSelectedOrganizationQuery()
-
const isBranch = Boolean(project?.parent_project_ref)
- const { projectSettingsRestartProject } = useIsFeatureEnabled([
- 'project_settings:restart_project',
- ])
-
const { can: canUpdateProject } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'projects', {
resource: {
project_id: project?.id,
@@ -86,184 +72,104 @@ export const General = () => {
}
return (
- <>
-
-
-
- General settings
-
-
-
- {isBranch && (
-
-
-
- You are currently on a preview branch of your project
-
-
- Certain settings are not available while you're on a preview branch. To adjust your
- project settings, you may return to your{' '}
-
- main branch
-
- .
-
-
- )}
+
+
+
+ General settings
+
+
+
+ {isBranch && (
+
+
+
+ You are currently on a preview branch of your project
+
+
+ Certain settings are not available while you're on a preview branch. To adjust your
+ project settings, you may return to your{' '}
+
+ main branch
+
+ .
+
+
+ )}
- {project === undefined ? (
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
- Project availability
-
- Restart or pause your project when performing maintenance
-
-
-
-
+ {project === undefined ? (
-
-
-
- {projectSettingsRestartProject ? 'Restart project' : 'Restart database'}
-
-
-
- Your project will not be available for a few minutes.
-
-
-
-
-
-
-
-
-
-
Pause project
-
-
- Your project will not be accessible while it is paused.
-
-
-
-
-
+
-
-
-
- {!isBranch && (
-
-
-
- Project usage
-
-
-
-
-
-
-
-
-
-
Project usage statistics have been moved
-
- You may view your project's usage under your organization's settings
-
-
-
-
- {!!organization && !!project && (
-
-
-
-
-
- )}
- >
+
+
+
+
+
+ )}
+
+
)
}
diff --git a/apps/studio/components/interfaces/Settings/General/Project.tsx b/apps/studio/components/interfaces/Settings/General/Project.tsx
new file mode 100644
index 00000000000..26d40787504
--- /dev/null
+++ b/apps/studio/components/interfaces/Settings/General/Project.tsx
@@ -0,0 +1,111 @@
+import { BarChart2 } from 'lucide-react'
+import Link from 'next/link'
+import { Button, Card, CardContent } from 'ui'
+import {
+ PageSection,
+ PageSectionContent,
+ PageSectionDescription,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns/PageSection'
+
+import PauseProjectButton from './Infrastructure/PauseProjectButton'
+import RestartServerButton from './Infrastructure/RestartServerButton'
+import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
+import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+
+export const Project = () => {
+ const { data: project } = useSelectedProjectQuery()
+ const { data: organization } = useSelectedOrganizationQuery()
+ const isBranch = Boolean(project?.parent_project_ref)
+ const { projectSettingsRestartProject } = useIsFeatureEnabled([
+ 'project_settings:restart_project',
+ ])
+
+ return (
+ <>
+
+
+
+ Project availability
+
+ Restart or pause your project when performing maintenance
+
+
+
+
+
+
+
+
+
+ {projectSettingsRestartProject ? 'Restart project' : 'Restart database'}
+
+
+
+ Your project will not be available for a few minutes.
+
+
+
+
+
+
+
+
+
+
Pause project
+
+
+ Your project will not be accessible while it is paused.
+
+
+
+
+
+
+
+
+
+
+ {!isBranch && (
+
+
+
+ Project usage
+
+
+
+
+
+
+
+
+
+
Project usage statistics have been moved
+
+ You may view your project's usage under your organization's settings
+
+
+
+
+ {!!organization && !!project && (
+
+ )}
+
+
+
+
+
+ )}
+ >
+ )
+}
diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx
index 7b37b7926ad..fa68d58cd64 100644
--- a/apps/studio/components/interfaces/Sidebar.tsx
+++ b/apps/studio/components/interfaces/Sidebar.tsx
@@ -258,9 +258,7 @@ const ProjectLinks = () => {
showReports,
apiDocsSidePanel: isNewAPIDocsEnabled,
})
- const settingsRoutes = generateSettingsRoutes(ref, project, {
- platformWebhooks: platformWebhooksEnabled,
- })
+ const settingsRoutes = generateSettingsRoutes(ref)
return (
diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.tsx
index a320c023817..37cb8e129bb 100644
--- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.tsx
@@ -99,7 +99,7 @@ export function MobileMenuContent({
}),
[ref, project, isUnifiedLogsEnabled, showReports, isNewAPIDocsEnabled]
)
- const settingsRoutes = useMemo(() => generateSettingsRoutes(ref, project), [ref, project])
+ const settingsRoutes = useMemo(() => generateSettingsRoutes(ref), [ref])
const homeRoute: Route = useMemo(
() => ({
diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
index 9b7262cadf0..0bab5712ebf 100644
--- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
@@ -1,5 +1,5 @@
import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar'
-import { generateSettingsMenu } from 'components/layouts/ProjectSettingsLayout/SettingsMenu.utils'
+import { useGenerateSettingsMenu } from 'components/layouts/ProjectSettingsLayout/SettingsMenu.utils'
import type { Route } from 'components/ui/ui.types'
import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor'
import type { Project } from 'data/projects/project-detail-query'
@@ -182,14 +182,7 @@ export const generateOtherRoutes = (
]
}
-export const generateSettingsRoutes = (
- ref?: string,
- project?: Project,
- features?: { platformWebhooks?: boolean }
-): Route[] => {
- const settingsMenu = generateSettingsMenu(ref as string, undefined, undefined, {
- platformWebhooks: features?.platformWebhooks,
- })
+export const generateSettingsRoutes = (ref?: string): Route[] => {
return [
{
key: 'settings',
@@ -198,7 +191,6 @@ export const generateSettingsRoutes = (
link:
ref &&
(IS_PLATFORM ? `/project/${ref}/settings/general` : `/project/${ref}/settings/log-drains`),
- items: settingsMenu,
disabled: false,
},
]
diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx
index 03b8c1ccce3..50ddecf86c9 100644
--- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx
+++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx
@@ -1,15 +1,10 @@
-import { useParams } from 'common'
-import { useIsPlatformWebhooksEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ProductMenu } from 'components/ui/ProductMenu'
-import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
-import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
-import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { withAuth } from 'hooks/misc/withAuth'
import { useRouter } from 'next/router'
import type { PropsWithChildren } from 'react'
import { ProjectLayout } from '../ProjectLayout'
-import { generateSettingsMenu } from './SettingsMenu.utils'
+import { useGenerateSettingsMenu } from './SettingsMenu.utils'
/**
* Menu-only component for the settings section. Used by the desktop sidebar and by the
@@ -18,48 +13,14 @@ import { generateSettingsMenu } from './SettingsMenu.utils'
*/
export const SettingsProductMenu = () => {
const router = useRouter()
- const { ref } = useParams()
- const { data: project } = useSelectedProjectQuery()
- const { data: organization } = useSelectedOrganizationQuery()
- const platformWebhooksEnabled = useIsPlatformWebhooksEnabled()
const page = router.pathname.includes('billing')
? router.pathname.split('/')[5]
: router.pathname.split('/')[4]
- const {
- projectAuthAll: authEnabled,
- authenticationShowProviders: authProvidersEnabled,
- projectEdgeFunctionAll: edgeFunctionsEnabled,
- projectStorageAll: storageEnabled,
- billingInvoices: invoicesEnabled,
- projectSettingsLegacyJwtKeys: legacyJWTKeysEnabled,
- projectSettingsLogDrains,
- billingAll,
- } = useIsFeatureEnabled([
- 'project_auth:all',
- 'authentication:show_providers',
- 'project_edge_function:all',
- 'project_storage:all',
- 'billing:invoices',
- 'project_settings:legacy_jwt_keys',
- 'project_settings:log_drains',
- 'billing:all',
- ])
+ const menu = useGenerateSettingsMenu()
- const menuRoutes = generateSettingsMenu(ref, project, organization, {
- auth: authEnabled,
- authProviders: authProvidersEnabled,
- edgeFunctions: edgeFunctionsEnabled,
- storage: storageEnabled,
- invoices: invoicesEnabled,
- legacyJwtKeys: legacyJWTKeysEnabled,
- logDrains: projectSettingsLogDrains,
- billing: billingAll,
- platformWebhooks: platformWebhooksEnabled,
- })
-
- return
+ return
}
interface SettingsLayoutProps {
diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx
index 7ed196c96dc..001e5ff6012 100644
--- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx
+++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx
@@ -1,5 +1,10 @@
+import { renderHook } from '@testing-library/react'
+import { useFlag } from 'common'
import { describe, expect, it, vi } from 'vitest'
+import { useGenerateSettingsMenu } from './SettingsMenu.utils'
+import { useIsPlatformWebhooksEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
+
vi.mock('lib/constants', async () => {
const actual = await vi.importActual>('lib/constants')
return {
@@ -8,18 +13,35 @@ vi.mock('lib/constants', async () => {
}
})
-import { generateSettingsMenu } from './SettingsMenu.utils'
+vi.mock('common', () => ({
+ useFlag: vi.fn().mockReturnValue(false),
+ useParams: vi.fn().mockReturnValue({ ref: 'project-ref' }),
+}))
-describe('generateSettingsMenu', () => {
- it('includes webhooks in project settings items', () => {
- const menu = generateSettingsMenu(
- 'project-ref',
- { status: 'ACTIVE_HEALTHY' } as any,
- { slug: 'my-org' } as any,
- { platformWebhooks: true }
- )
+vi.mock('@/hooks/misc/useSelectedOrganization', () => ({
+ useSelectedOrganizationQuery: vi.fn().mockReturnValue({ data: { slug: 'my-org' } }),
+}))
- const configurationGroup = menu.find((group) => group.title === 'Configuration')
+vi.mock('@/hooks/misc/useSelectedProject', () => ({
+ useSelectedProjectQuery: vi.fn().mockReturnValue({ data: { status: 'ACTIVE_HEALTHY' } }),
+}))
+
+vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({
+ useIsFeatureEnabled: vi
+ .fn()
+ .mockReturnValue({ projectSettingsLegacyJwtKeys: false, billingAll: true }),
+}))
+
+vi.mock('@/components/interfaces/App/FeaturePreview/FeaturePreviewContext', () => ({
+ useIsPlatformWebhooksEnabled: vi.fn().mockReturnValue(true),
+}))
+
+describe('useGenerateSettingsMenu', () => {
+ it('includes webhooks when platformWebhooks feature is enabled', () => {
+ vi.mocked(useIsPlatformWebhooksEnabled).mockReturnValue(true)
+
+ const { result } = renderHook(() => useGenerateSettingsMenu())
+ const configurationGroup = result.current.find((group) => group.title === 'Configuration')
const hasWebhooks = configurationGroup?.items.some(
(item) => item.name === 'Webhooks' && item.url === '/project/project-ref/settings/webhooks'
)
@@ -28,16 +50,35 @@ describe('generateSettingsMenu', () => {
})
it('hides webhooks when platformWebhooks feature is disabled', () => {
- const menu = generateSettingsMenu(
- 'project-ref',
- { status: 'ACTIVE_HEALTHY' } as any,
- { slug: 'my-org' } as any,
- { platformWebhooks: false }
- )
+ vi.mocked(useIsPlatformWebhooksEnabled).mockReturnValue(false)
- const configurationGroup = menu.find((group) => group.title === 'Configuration')
+ const { result } = renderHook(() => useGenerateSettingsMenu())
+ const configurationGroup = result.current.find((group) => group.title === 'Configuration')
const hasWebhooks = configurationGroup?.items.some((item) => item.name === 'Webhooks')
expect(hasWebhooks).toBe(false)
})
+
+ it('includes dashboard preferences when flag is enabled', () => {
+ vi.mocked(useFlag).mockReturnValue(true)
+
+ const { result } = renderHook(() => useGenerateSettingsMenu())
+ const preferencesGroup = result.current.find((group) => group.title === 'Preferences')
+ const hasDashboardPreferences = preferencesGroup?.items.some(
+ (item) =>
+ item.name === 'Dashboard preferences' &&
+ item.url === '/project/project-ref/settings/preferences'
+ )
+
+ expect(hasDashboardPreferences).toBe(true)
+ })
+
+ it('hides dashboard preferences when flag is disabled', () => {
+ vi.mocked(useFlag).mockReturnValue(false)
+
+ const { result } = renderHook(() => useGenerateSettingsMenu())
+ const preferencesGroup = result.current.find((group) => group.title === 'Preferences')
+
+ expect(preferencesGroup).toBeUndefined()
+ })
})
diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx
index 7b6e2aec480..da1ce946586 100644
--- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx
+++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx
@@ -1,25 +1,25 @@
-import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
-import type { Project } from 'data/projects/project-detail-query'
+import { useFlag, useParams } from 'common'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import { ArrowUpRight } from 'lucide-react'
-import type { Organization } from 'types'
-export const generateSettingsMenu = (
- ref?: string,
- project?: Project,
- organization?: Organization,
- features?: {
- auth?: boolean
- authProviders?: boolean
- edgeFunctions?: boolean
- storage?: boolean
- invoices?: boolean
- legacyJwtKeys?: boolean
- logDrains?: boolean
- billing?: boolean
- platformWebhooks?: boolean
- }
-): ProductMenuGroup[] => {
+import { useIsPlatformWebhooksEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
+import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
+import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+
+export const useGenerateSettingsMenu = () => {
+ const { ref } = useParams()
+ const { data: project } = useSelectedProjectQuery()
+ const { data: organization } = useSelectedOrganizationQuery()
+
+ const showDashboardPreferences = useFlag('dashboardPreferences')
+ const platformWebhooksEnabled = useIsPlatformWebhooksEnabled()
+
+ const { projectSettingsLegacyJwtKeys: legacyJwtKeysEnabled, billingAll: billingEnabled } =
+ useIsFeatureEnabled(['project_settings:legacy_jwt_keys', 'billing:all'])
+
+ const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
+
if (!IS_PLATFORM) {
return [
{
@@ -36,12 +36,6 @@ export const generateSettingsMenu = (
]
}
- const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
-
- const legacyJwtKeysEnabled = features?.legacyJwtKeys ?? true
- const billingEnabled = features?.billing ?? true
- const platformWebhooksEnabled = features?.platformWebhooks ?? false
-
return [
{
title: 'Configuration',
@@ -118,6 +112,21 @@ export const generateSettingsMenu = (
},
],
},
+ ...(IS_PLATFORM && showDashboardPreferences
+ ? [
+ {
+ title: 'Preferences',
+ items: [
+ {
+ name: 'Dashboard preferences',
+ key: 'preferences',
+ url: `/project/${ref}/settings/preferences`,
+ items: [],
+ },
+ ],
+ },
+ ]
+ : []),
{
title: 'Integrations',
items: [
diff --git a/apps/studio/components/ui/DatabaseSelector.tsx b/apps/studio/components/ui/DatabaseSelector.tsx
index 23143762b62..bf73976adab 100644
--- a/apps/studio/components/ui/DatabaseSelector.tsx
+++ b/apps/studio/components/ui/DatabaseSelector.tsx
@@ -37,6 +37,7 @@ interface DatabaseSelectorProps {
onSelectId?: (id: string) => void // Optional callback
className?: string
align?: 'start' | 'end'
+ isForm?: boolean
}
export const DatabaseSelector = ({
@@ -47,6 +48,7 @@ export const DatabaseSelector = ({
buttonProps,
align = 'end',
className,
+ isForm = false,
}: DatabaseSelectorProps) => {
const router = useRouter()
const { ref: projectRef } = useParams()
@@ -73,7 +75,7 @@ export const DatabaseSelector = ({
const newReplicaURL = `/project/${projectRef}/database/replication?type=Read+Replica`
useEffect(() => {
- if (_selectedDatabaseId) state.setSelectedDatabaseId(_selectedDatabaseId)
+ if (_selectedDatabaseId && !isForm) state.setSelectedDatabaseId(_selectedDatabaseId)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [_selectedDatabaseId])
@@ -81,16 +83,21 @@ export const DatabaseSelector = ({
-
- Source
-
+ {!isForm && (
+
+ Source
+
+ )}
}
- iconRight={
}
+ iconRight={
+
+ }
{...buttonProps}
className={cn(
- 'pr-2 rounded-l-none',
+ 'pr-2 justify-start',
+ !isForm && 'rounded-l-none',
variant === 'connected-on-right' && 'rounded-r-none',
variant === 'connected-on-left' && 'rounded-l-none border-l-0',
variant === 'connected-on-both' && 'rounded-none border-x-0',
@@ -127,12 +134,12 @@ export const DatabaseSelector = ({
value={option.id}
className="cursor-pointer w-full"
onSelect={() => {
- state.setSelectedDatabaseId(option.id)
+ if (!isForm) state.setSelectedDatabaseId(option.id)
setOpen(false)
onSelectId(option.id)
}}
onClick={() => {
- state.setSelectedDatabaseId(option.id)
+ if (!isForm) state.setSelectedDatabaseId(option.id)
setOpen(false)
onSelectId(option.id)
}}
@@ -184,12 +191,12 @@ export const DatabaseSelector = ({
value={database.identifier}
className="cursor-pointer w-full"
onSelect={() => {
- state.setSelectedDatabaseId(database.identifier)
+ if (!isForm) state.setSelectedDatabaseId(database.identifier)
setOpen(false)
onSelectId(database.identifier)
}}
onClick={() => {
- state.setSelectedDatabaseId(database.identifier)
+ if (!isForm) state.setSelectedDatabaseId(database.identifier)
setOpen(false)
onSelectId(database.identifier)
}}
diff --git a/apps/studio/components/ui/UpgradePlanButton.tsx b/apps/studio/components/ui/UpgradePlanButton.tsx
index 186feac161e..1842ab7f0e7 100644
--- a/apps/studio/components/ui/UpgradePlanButton.tsx
+++ b/apps/studio/components/ui/UpgradePlanButton.tsx
@@ -73,7 +73,13 @@ export const UpgradePlanButton = ({
: `/project/${ref ?? '_'}/settings/addons?panel=${addon}&source=${source}`
: `/org/${slug ?? '_'}/billing?panel=subscriptionPlan&source=${source}`
- const linkChildren = children || (!!addon ? 'Enable add-on' : `Upgrade to ${plan}`)
+ const linkChildren =
+ children ||
+ (isOnPaidPlanAndRequestingToPurchaseAddon
+ ? addon === 'computeSize'
+ ? 'Change compute size'
+ : 'Enable add-on'
+ : `Upgrade to ${plan}`)
const link = billingAll ? (
{linkChildren}
) : (
diff --git a/apps/studio/data/read-replicas/replicas-query.ts b/apps/studio/data/read-replicas/replicas-query.ts
index e4dc14ba655..a119c3b4853 100644
--- a/apps/studio/data/read-replicas/replicas-query.ts
+++ b/apps/studio/data/read-replicas/replicas-query.ts
@@ -1,10 +1,12 @@
import { useQuery } from '@tanstack/react-query'
-import { useFeatureFlags, useFlag } from 'common'
+import { LOCAL_STORAGE_KEYS, useFeatureFlags, useFlag, useParams } from 'common'
import type { components } from 'data/api'
import { get, handleError } from 'data/fetchers'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { replicaKeys } from './keys'
+import { DashboardPreference } from '@/components/interfaces/Settings/General/DashboardPreferences'
+import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
export const MAX_REPLICAS_BELOW_XL = 2
@@ -69,29 +71,36 @@ export const useConnectionStringForReadOps = (): {
identifier: string | undefined
connectionString: string | undefined | null
} => {
+ const { ref: projectRef } = useParams()
const { hasLoaded: flagsLoaded } = useFeatureFlags()
- const defaultToReadReplicaConnectionString = useFlag('defaultToReadReplicaConnectionString')
+ const showDashboardPreferences = useFlag('dashboardPreferences')
+
+ const [dashboardPreferences, , { isLoading: isLoadingLocalStorage }] =
+ useLocalStorageQuery
(
+ LOCAL_STORAGE_KEYS.DASHBOARD_PREFERENCES(projectRef ?? '_'),
+ {}
+ )
const { data: project, isSuccess: isSuccessProject } = useSelectedProjectQuery()
const { data: databases = [], isLoading: isLoadingDatabases } = useReadReplicasQuery({
projectRef: project?.ref,
})
- const readReplicas = databases.filter(
- (x) => x.identifier !== project?.ref && x.status === 'ACTIVE_HEALTHY'
- )
- const readReplica = readReplicas.some((x) => x.region === project?.region)
- ? readReplicas.find((x) => x.region === project?.region)
- : readReplicas[0]
-
- if (!isSuccessProject || isLoadingDatabases || !flagsLoaded) {
+ if (!isSuccessProject || isLoadingDatabases || !flagsLoaded || isLoadingLocalStorage) {
return { connectionString: undefined, type: undefined, identifier: undefined }
}
- if (!defaultToReadReplicaConnectionString) {
+ if (!showDashboardPreferences) {
return { type: 'primary', identifier: project.ref, connectionString: project.connectionString }
}
+ const readReplicas = databases.filter(
+ (x) => x.identifier !== project?.ref && x.status === 'ACTIVE_HEALTHY'
+ )
+ const readReplica = readReplicas.find(
+ (x) => x.identifier === dashboardPreferences.defaultDatabase
+ )
+
return {
type: !!readReplica ? 'replica' : 'primary',
identifier: !!readReplica ? readReplica.identifier : project.ref,
diff --git a/apps/studio/pages/project/[ref]/settings/general.tsx b/apps/studio/pages/project/[ref]/settings/general.tsx
index 433e9053ee0..31306bbe406 100644
--- a/apps/studio/pages/project/[ref]/settings/general.tsx
+++ b/apps/studio/pages/project/[ref]/settings/general.tsx
@@ -23,6 +23,8 @@ import {
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
+import { Project } from '@/components/interfaces/Settings/General/Project'
+
const ProjectSettings: NextPageWithLayout = () => {
const { data: project } = useSelectedProjectQuery()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
@@ -55,7 +57,7 @@ const ProjectSettings: NextPageWithLayout = () => {
-
+
{/* this is only settable on compliance orgs, currently that means HIPAA orgs */}
{!isBranch && hasHipaaAddon && }
{projectSettingsCustomDomains && }
diff --git a/apps/studio/pages/project/[ref]/settings/index.tsx b/apps/studio/pages/project/[ref]/settings/index.tsx
deleted file mode 100644
index cc72e8e673c..00000000000
--- a/apps/studio/pages/project/[ref]/settings/index.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import DefaultLayout from 'components/layouts/DefaultLayout'
-import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
-import type { NextPageWithLayout } from 'types'
-
-const Settings: NextPageWithLayout = () => {
- return <>{/* Use this as a page template for settings pages
*/}>
-}
-
-Settings.getLayout = (page) => (
-
- {page}
-
-)
-
-export default Settings
diff --git a/apps/studio/pages/project/[ref]/settings/preferences.tsx b/apps/studio/pages/project/[ref]/settings/preferences.tsx
new file mode 100644
index 00000000000..08ecba7527d
--- /dev/null
+++ b/apps/studio/pages/project/[ref]/settings/preferences.tsx
@@ -0,0 +1,40 @@
+import DefaultLayout from 'components/layouts/DefaultLayout'
+import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
+import type { NextPageWithLayout } from 'types'
+import { PageContainer } from 'ui-patterns/PageContainer'
+import {
+ PageHeader,
+ PageHeaderDescription,
+ PageHeaderMeta,
+ PageHeaderSummary,
+ PageHeaderTitle,
+} from 'ui-patterns/PageHeader'
+
+import { DashboardPreferences } from '@/components/interfaces/Settings/General/DashboardPreferences'
+
+const Preferences: NextPageWithLayout = () => {
+ return (
+ <>
+
+
+
+ Dashboard Preferences
+
+ Configure how the dashboard interacts with your project
+
+
+
+
+
+
+
+ >
+ )
+}
+
+Preferences.getLayout = (page) => (
+
+ {page}
+
+)
+export default Preferences
diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts
index d484857f70c..98f168b4633 100644
--- a/packages/common/constants/local-storage.ts
+++ b/packages/common/constants/local-storage.ts
@@ -13,6 +13,7 @@ export const LOCAL_STORAGE_KEYS = {
FEEDBACK_WIDGET_SCREENSHOT: 'feedback-widget-screenshot',
INCIDENT_BANNER_DISMISSED_IDS: 'incident-banner-dismissed-ids',
MAINTENANCE_BANNER_DISMISSED: (id: string) => `maintenance-banner-dismissed-${id}`,
+ DASHBOARD_PREFERENCES: (ref: string) => `dashboard-preferences-${ref}`,
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
UI_PREVIEW_CLS: 'supabase-ui-cls',