Files
supabase/apps/studio/components/ui/DatabaseSelector.tsx
Danny White 6ac5bf6b86 feat(studio): point replica deep links at Infrastructure and recommend compute (#48921)
## What kind of change does this PR introduce?

Feature. Stack 5 of 5 (tip) for
[PIPE-1007](https://linear.app/supabase/issue/PIPE-1007/move-read-replicas-out-of-replication-into-infrastructure).

## What is the current behavior?

Selectors still open Replication with `destinationType=Read+Replica`.
Compute eligibility actions leave the add-replica flow without carrying
a recommended size into the Infrastructure form.

## What is the new behavior?

DatabaseSelector and the SQL submenu open Infrastructure
`?addReplica=true`. Change to Small/XL compute waits for the sheet to
close, pre-selects that size, then focuses and scrolls the
Infrastructure form to Compute without shifting the page footer.

## Additional context

Last stack PR. [#49043](https://github.com/supabase/supabase/pull/49043)
has merged. Please review, but do not merge until 2→4 are also approved.
Then merge [#49044](https://github.com/supabase/supabase/pull/49044) →
[#49045](https://github.com/supabase/supabase/pull/49045) →
[#49046](https://github.com/supabase/supabase/pull/49046) → this PR in
succession, and drop the `do-not-merge` labels.

Update the read replicas getting-started doc in the same sitting so it
points only at Infrastructure (it currently also links Database →
Replication).

Remaining stack:
[#49044](https://github.com/supabase/supabase/pull/49044) →
[#49045](https://github.com/supabase/supabase/pull/49045) →
[#49046](https://github.com/supabase/supabase/pull/49046) → this PR

## To test

`infrastructure:read_replicas` is an enabled-feature, on by default.
There is no Feature Preview or ConfigCat switch. You should already see
the Infrastructure Read replicas section. If you do not, your profile
lists `infrastructure:read_replicas` in `disabled_features`.

1.
[Infrastructure](https://studio-staging-git-dnywh-choreread-replicas-in-829a4b-supabase.vercel.app/dashboard/project/_/settings/infrastructure):
topology, Read replicas, Scaling.
2. Add read replica. If blocked on compute, Change to Small compute:
sheet closes, Small is selected and focused, the price footer is dirty,
and no blank page gap appears.
3. From the SQL editor database selector, Add replica should open
Infrastructure, not Replication.
2026-08-21 12:44:41 +10:00

252 lines
9.8 KiB
TypeScript

import { useParams } from 'common'
import { noop } from 'lodash'
import { Check, ChevronDown, Loader2, Plus } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useEffect, useState } from 'react'
import {
Button,
ButtonProps,
cn,
Command,
CommandGroup,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
ScrollArea,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { Markdown } from '@/components/interfaces/Markdown'
import {
getAddReadReplicaPath,
getInfrastructurePath,
} from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils'
import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicas.constants'
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { formatDatabaseID, formatDatabaseRegion } from '@/data/read-replicas/replicas.utils'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { IS_PLATFORM } from '@/lib/constants'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
interface DatabaseSelectorProps {
selectedDatabaseId?: string // To override initial state
variant?: 'regular' | 'connected-on-right' | 'connected-on-left' | 'connected-on-both'
additionalOptions?: { id: string; name: string }[]
buttonProps?: ButtonProps
onSelectId?: (id: string) => void // Optional callback
className?: string
align?: 'start' | 'end'
isForm?: boolean
}
export const DatabaseSelector = ({
selectedDatabaseId: _selectedDatabaseId,
variant = 'regular',
additionalOptions = [],
onSelectId = noop,
buttonProps,
align = 'end',
className,
isForm = false,
}: DatabaseSelectorProps) => {
const router = useRouter()
const { ref: projectRef } = useParams()
const [open, setOpen] = useState(false)
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
const state = useDatabaseSelectorStateSnapshot()
const selectedDatabaseId = _selectedDatabaseId ?? state.selectedDatabaseId
const { data, isPending: isLoading, isSuccess } = useReadReplicasQuery({ projectRef })
const databases = data ?? []
const sortedDatabases = databases
.sort((a, b) => (a.inserted_at > b.inserted_at ? 1 : 0))
.sort((database) => (database.identifier === projectRef ? -1 : 0))
const selectedDatabase = databases.find((db) => db.identifier === selectedDatabaseId)
const selectedDatabaseRegion = formatDatabaseRegion(selectedDatabase?.region ?? '')
const formattedDatabaseId = formatDatabaseID(selectedDatabaseId ?? '')
const selectedAdditionalOption = additionalOptions.find((x) => x.id === selectedDatabaseId)
const newReplicaURL = getAddReadReplicaPath(projectRef)
useEffect(() => {
if (_selectedDatabaseId && !isForm) state.setSelectedDatabaseId(_selectedDatabaseId)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [_selectedDatabaseId])
return (
<Popover open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger asChild>
<div className={cn('flex cursor-pointer', className)}>
{!isForm && (
<span className="flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
Source
</span>
)}
<Button
variant="default"
icon={isLoading && <Loader2 className="animate-spin" />}
iconRight={<ChevronDown strokeWidth={1.5} size={12} />}
{...buttonProps}
className={cn(
'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',
buttonProps?.className
)}
>
{selectedAdditionalOption ? (
<span>{selectedAdditionalOption.name}</span>
) : (
<>
<span className="capitalize">
{isLoading || selectedDatabase?.identifier === projectRef
? 'Primary database'
: 'Read replica'}
</span>{' '}
{isSuccess && selectedDatabase?.identifier !== projectRef && (
<span>
({selectedDatabaseRegion} - {formattedDatabaseId})
</span>
)}
</>
)}
</Button>
</div>
</PopoverTrigger>
<PopoverContent className="p-0 w-64" side="bottom" align={align}>
<Command>
<CommandList>
{additionalOptions.length > 0 && (
<CommandGroup className="border-b">
{additionalOptions.map((option) => (
<CommandItem
key={option.id}
value={option.id}
className="cursor-pointer w-full"
onSelect={() => {
if (!isForm) state.setSelectedDatabaseId(option.id)
setOpen(false)
onSelectId(option.id)
}}
onClick={() => {
if (!isForm) state.setSelectedDatabaseId(option.id)
setOpen(false)
onSelectId(option.id)
}}
>
<div className="w-full flex items-center justify-between">
<p>{option.name}</p>
{option.id === selectedDatabaseId && <Check size={14} />}
</div>
</CommandItem>
))}
</CommandGroup>
)}
<CommandGroup>
<ScrollArea className={(databases || []).length > 7 ? 'h-[210px]' : ''}>
{sortedDatabases?.map((database) => {
const region = formatDatabaseRegion(database.region)
const id = formatDatabaseID(database.identifier)
if (database.status !== 'ACTIVE_HEALTHY') {
const status = [
REPLICA_STATUS.INIT_READ_REPLICA,
REPLICA_STATUS.COMING_UP,
].includes(database.status)
? 'coming up'
: 'not healthy'
return (
<Tooltip key={database.identifier}>
<TooltipTrigger asChild>
<div className="px-2 py-1.5 w-full flex items-center justify-between">
<p className="text-xs text-foreground-lighter">
Read replica ({region} - {id})
</p>
</div>
</TooltipTrigger>
<TooltipContent side="right" className="w-80">
<Markdown
className="text-xs text-foreground"
content={`Replica unable to accept requests as its ${status}. [View infrastructure settings](${getInfrastructurePath(projectRef)}) for more information.`}
/>
</TooltipContent>
</Tooltip>
)
}
return (
<CommandItem
key={database.identifier}
value={database.identifier}
className="cursor-pointer w-full"
onSelect={() => {
if (!isForm) state.setSelectedDatabaseId(database.identifier)
setOpen(false)
onSelectId(database.identifier)
}}
onClick={() => {
if (!isForm) state.setSelectedDatabaseId(database.identifier)
setOpen(false)
onSelectId(database.identifier)
}}
>
<div className="w-full flex items-center justify-between">
<p>
{database.identifier === projectRef
? 'Primary database'
: `Read replica (${region} - ${id})`}
</p>
{database.identifier === selectedDatabaseId && <Check size={16} />}
</div>
</CommandItem>
)
})}
</ScrollArea>
</CommandGroup>
{IS_PLATFORM && infrastructureReadReplicas && (
<CommandGroup className="border-t">
<CommandItem
className="cursor-pointer w-full"
onSelect={() => {
setOpen(false)
router.push(newReplicaURL)
}}
onClick={() => setOpen(false)}
>
<Link
href={newReplicaURL}
onClick={async () => {
setOpen(false)
// [Joshen] This is used in the Connect UI which is available across all pages
setShowConnect(false)
}}
className="w-full flex items-center gap-2"
>
<Plus size={14} strokeWidth={1.5} />
<p>Create a new read replica</p>
</Link>
</CommandItem>
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}