mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 18:35:07 +03:00
## 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.
252 lines
9.8 KiB
TypeScript
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>
|
|
)
|
|
}
|