From 0b97e0ff86aaac4fc73dce80ec9ff0be2f3087df Mon Sep 17 00:00:00 2001
From: Joshen Lim
Date: Thu, 12 Mar 2026 11:18:28 +0700
Subject: [PATCH] Joshen/fe 2642 project settings allow to select a replica to
power (#43626)
## Context
Related to dashboard scalability
Previous PR: https://github.com/supabase/supabase/pull/42856
Note: Changes are all feature flagged still and I'm still not entirely
convinced with the current UX
Will iterate as as go along, and only make this publicly available when
we're satisfied with the behaviour
Adds a "Dashboard preference" section to the project settings
In which users can then select which database they'd like to use for
read queries run from the dashboard
Note: Everything is local storage for now, but we'd need middleware
support if we want to make this setting persist for all users on the
project
Added a dialog as well to further explain what this implies
## To test
- Choosing a replica in dashboard preferences will only affect the table
editor as thats the only place that's set up so far to use a replica for
read queries (I'll need to follow up for other parts of the dashboard in
subsequent PRs)
---
.../Connect/DatabaseConnectionString.tsx | 2 +-
.../ReadReplicaEligibilityWarnings.tsx | 12 +-
.../Settings/General/DashboardPreferences.tsx | 194 ++++++++++++
.../interfaces/Settings/General/General.tsx | 290 ++++++------------
.../interfaces/Settings/General/Project.tsx | 111 +++++++
apps/studio/components/interfaces/Sidebar.tsx | 4 +-
.../MobileMenuContent/MobileMenuContent.tsx | 2 +-
.../NavigationBar/NavigationBar.utils.tsx | 12 +-
.../ProjectSettingsLayout/SettingsLayout.tsx | 45 +--
.../SettingsMenu.utils.test.tsx | 75 ++++-
.../SettingsMenu.utils.tsx | 59 ++--
.../studio/components/ui/DatabaseSelector.tsx | 27 +-
.../components/ui/UpgradePlanButton.tsx | 8 +-
.../data/read-replicas/replicas-query.ts | 31 +-
.../pages/project/[ref]/settings/general.tsx | 4 +-
.../pages/project/[ref]/settings/index.tsx | 15 -
.../project/[ref]/settings/preferences.tsx | 40 +++
packages/common/constants/local-storage.ts | 1 +
18 files changed, 596 insertions(+), 336 deletions(-)
create mode 100644 apps/studio/components/interfaces/Settings/General/DashboardPreferences.tsx
create mode 100644 apps/studio/components/interfaces/Settings/General/Project.tsx
delete mode 100644 apps/studio/pages/project/[ref]/settings/index.tsx
create mode 100644 apps/studio/pages/project/[ref]/settings/preferences.tsx
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 ? (
-