chore(studio): Replication UI updates for new replica CTA + URL query state handling (#42378)

## Context

Related to unifying read replicas into database replication page which
is currently in internal testing

## Changes involved

- Updates the "New replica" CTA in `DatabaseSelector` (e.g within the
SQL Editor) to link to the replication page instead of settings
infrastructure
- Refactor replication UI to use URL query states for new destination +
edit destination

## To test

- [ ] Verify that the "New replica" CTA links correctly to replication
page if flag is on
- [ ] Verify that new + edit destination UI works as expected
- Note that there's some server side validation for ETL replication now
so might be tricky to test creating an actual ETL replication
- Minimally can test creating a read replica and ensure that the UI
behaviour is all as expected

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added unified replication experience with URL-based state management
for destination creation and editing

* **Refactor**
* Simplified replication panel component interfaces and consolidated
destination data fetching logic
* Enhanced edit flows to leverage URL parameters for seamless navigation

* **Chores**
  * Marked legacy read replica creation panel as deprecated
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-02-03 15:54:21 +08:00
1 parent 8233902aef
commit 6ffc3fc94c
11 files changed
+257 -165

No files matched your search

@@ -1,8 +1,10 @@
import { useFlag, useParams } from 'common'
import { useFlag } from 'common'
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { ArrowUpRight } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { toast } from 'sonner'
import {
Button,
DialogSectionSeparator,
@@ -16,52 +18,81 @@ import {
} from 'ui'
import { EnableReplicationCallout } from '../EnableReplicationCallout'
import { PipelineStatusName } from '../Replication.constants'
import { useDestinationInformation } from '../useDestinationInformation'
import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha'
import { DestinationForm } from './DestinationForm'
import { DestinationType } from './DestinationPanel.types'
import { DestinationTypeSelection } from './DestinationTypeSelection'
import { ReadReplicaForm } from './ReadReplicaForm'
import { DocsButton } from '@/components/ui/DocsButton'
import { useReplicationSourcesQuery } from '@/data/replication/sources-query'
import { DOCS_URL } from '@/lib/constants'
interface DestinationPanelProps {
visible: boolean
type?: DestinationType
existingDestination?: {
sourceId?: number
destinationId: number
pipelineId?: number
enabled: boolean
statusName?: string
}
onClose: () => void
onSuccessCreateReadReplica?: () => void
}
export const DestinationPanel = ({
visible,
type,
existingDestination,
onClose,
onSuccessCreateReadReplica,
}: DestinationPanelProps) => {
const { ref: projectRef } = useParams()
export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPanelProps) => {
const enablePgReplicate = useIsETLPrivateAlpha()
const unifiedReplication = useFlag('unifiedReplication')
const { hasAccess: hasETLReplicationAccess } = useCheckEntitlements('replication.etl')
const [selectedType, setSelectedType] = useState<DestinationType>(
type || (unifiedReplication ? 'Read Replica' : 'BigQuery')
const [urlDestinationType, setDestinationType] = useQueryState(
'type',
parseAsStringEnum<DestinationType>([
'Read Replica',
'BigQuery',
'Analytics Bucket',
]).withOptions({
history: 'push',
clearOnDefault: true,
})
)
const editMode = !!existingDestination
const [edit, setEdit] = useQueryState(
'edit',
parseAsInteger.withOptions({
history: 'push',
clearOnDefault: true,
})
)
const { data: sourcesData, isSuccess: isSourcesSuccess } = useReplicationSourcesQuery({
projectRef,
})
const sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id
const replicationNotEnabled = isSourcesSuccess && !sourceId
const visible = urlDestinationType !== null || edit !== null
const editMode = edit !== null
const {
sourceId,
pipeline,
statusName,
replicationNotEnabled,
type: existingDestinationType,
destinationFetcher,
} = useDestinationInformation({ id: edit })
const destinationType = existingDestinationType ?? urlDestinationType
const invalidExistingDestination = destinationFetcher.error?.code === 404
const existingDestination = editMode
? {
sourceId,
destinationId: edit,
pipelineId: pipeline?.id,
statusName,
enabled:
statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED,
}
: undefined
const onClose = () => {
setDestinationType(null)
setEdit(null)
}
useEffect(() => {
if (edit !== null && invalidExistingDestination) {
toast(`Unable to find destination ID ${edit}`)
setEdit(null)
}
}, [edit, invalidExistingDestination, setEdit])
return (
<>
@@ -81,15 +112,11 @@ export const DestinationPanel = ({
</SheetDescription>
</SheetHeader>
<DestinationTypeSelection
editMode={editMode}
selectedType={selectedType}
setSelectedType={setSelectedType}
/>
<DestinationTypeSelection />
<DialogSectionSeparator />
{selectedType === 'Read Replica' ? (
{destinationType === 'Read Replica' ? (
<ReadReplicaForm onClose={onClose} onSuccess={() => onSuccessCreateReadReplica?.()} />
) : unifiedReplication && !enablePgReplicate ? (
<SheetSection>
@@ -124,14 +151,14 @@ export const DestinationPanel = ({
<SheetSection>
<EnableReplicationCallout
className="!p-6"
type={selectedType}
type={destinationType}
hasAccess={hasETLReplicationAccess}
/>
</SheetSection>
) : (
<DestinationForm
visible={visible}
selectedType={selectedType}
selectedType={destinationType ?? 'Read Replica'}
existingDestination={existingDestination}
onClose={onClose}
/>
@@ -1,22 +1,14 @@
import { useFlag } from 'common'
import { AnalyticsBucket, BigQuery, Database } from 'icons'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
import { Badge, RadioGroupStacked, RadioGroupStackedItem, cn } from 'ui'
import { useDestinationInformation } from '../useDestinationInformation'
import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha'
import { DestinationType } from './DestinationPanel.types'
import { InlineLink } from '@/components/ui/InlineLink'
type DestinationTypeSelectionProps = {
editMode: boolean
selectedType: DestinationType
setSelectedType: (value: DestinationType) => void
}
export const DestinationTypeSelection = ({
editMode,
selectedType,
setSelectedType,
}: DestinationTypeSelectionProps) => {
export const DestinationTypeSelection = () => {
const enablePgReplicate = useIsETLPrivateAlpha()
const unifiedReplication = useFlag('unifiedReplication')
const etlEnableBigQuery = useFlag('etlEnableBigQuery')
@@ -26,6 +18,27 @@ export const DestinationTypeSelection = ({
Boolean
).length
const [urlDestinationType, setDestinationType] = useQueryState(
'type',
parseAsStringEnum<DestinationType>([
'Read Replica',
'BigQuery',
'Analytics Bucket',
]).withOptions({
history: 'push',
clearOnDefault: true,
})
)
const [edit] = useQueryState(
'edit',
parseAsInteger.withOptions({ history: 'push', clearOnDefault: true })
)
const editMode = edit !== null
const { type: existingDestinationType } = useDestinationInformation({ id: edit })
const destinationType = existingDestinationType ?? urlDestinationType
return (
<div className="px-5 py-5">
<div className="flex flex-col gap-y-1 mb-4">
@@ -36,8 +49,8 @@ export const DestinationTypeSelection = ({
</div>
<RadioGroupStacked
disabled={editMode}
value={selectedType}
onValueChange={(value) => setSelectedType(value as DestinationType)}
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',
@@ -45,7 +58,8 @@ export const DestinationTypeSelection = ({
'[&>button:first-of-type]:!rounded-l-lg [&>button:last-of-type]:!rounded-r-lg'
)}
>
{((!editMode && unifiedReplication) || (editMode && selectedType === 'Read Replica')) && (
{((!editMode && unifiedReplication) ||
(editMode && destinationType === 'Read Replica')) && (
<RadioGroupStackedItem
label=""
showIndicator={false}
@@ -65,7 +79,7 @@ export const DestinationTypeSelection = ({
</RadioGroupStackedItem>
)}
{((!editMode && etlEnableBigQuery) || (editMode && selectedType === 'BigQuery')) && (
{((!editMode && etlEnableBigQuery) || (editMode && destinationType === 'BigQuery')) && (
<RadioGroupStackedItem label="" showIndicator={false} id="BigQuery" value="BigQuery">
<div className="flex flex-col gap-y-2">
<BigQuery size={20} />
@@ -82,7 +96,8 @@ export const DestinationTypeSelection = ({
</RadioGroupStackedItem>
)}
{((!editMode && etlEnableIceberg) || (editMode && selectedType === 'Analytics Bucket')) && (
{((!editMode && etlEnableIceberg) ||
(editMode && destinationType === 'Analytics Bucket')) && (
<RadioGroupStackedItem
label=""
showIndicator={false}
@@ -106,7 +121,7 @@ export const DestinationTypeSelection = ({
)}
</RadioGroupStacked>
{selectedType !== 'Read Replica' && enablePgReplicate && (
{destinationType !== 'Read Replica' && enablePgReplicate && (
<p className="mt-3 text-sm text-foreground-light">
Replication is in alpha. Expect rapid changes and possible breaking updates.{' '}
<InlineLink href="https://github.com/orgs/supabase/discussions/39416">
@@ -1,22 +1,19 @@
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { AlertError } from 'components/ui/AlertError'
import { useDeleteDestinationPipelineMutation } from 'data/replication/delete-destination-pipeline-mutation'
import { useReplicationPipelineReplicationStatusQuery } from 'data/replication/pipeline-replication-status-query'
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
import { useReplicationPipelineVersionQuery } from 'data/replication/pipeline-version-query'
import { Pipeline } from 'data/replication/pipelines-query'
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
import { AnalyticsBucket, BigQuery, Database } from 'icons'
import { Minus } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import {
PipelineStatusRequestStatus,
usePipelineRequestStatus,
} from 'state/replication-pipeline-request-status'
import type { ResponseError } from 'types'
import {
Button,
TableCell,
@@ -27,46 +24,38 @@ import {
WarningIcon,
} from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { DeleteDestination } from './DeleteDestination'
import { DestinationPanel } from './DestinationPanel/DestinationPanel'
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
import { getStatusName, PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
import { PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
import { PipelineStatus } from './PipelineStatus'
import { PipelineStatusName, STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants'
import { RowMenu } from './RowMenu'
import { UpdateVersionModal } from './UpdateVersionModal'
import { useDestinationInformation } from './useDestinationInformation'
interface DestinationRowProps {
sourceId?: number
destinationId: number
destinationName: string
type?: DestinationType
pipeline?: Pipeline
error: ResponseError | null
isLoading: boolean
isError: boolean
isSuccess: boolean
}
export const DestinationRow = ({
sourceId,
destinationId,
destinationName,
type,
pipeline,
error: pipelineError,
isLoading: isPipelineLoading,
isError: isPipelineError,
isSuccess: isPipelineSuccess,
}: DestinationRowProps) => {
export const DestinationRow = ({ destinationId }: DestinationRowProps) => {
const { ref: projectRef } = useParams()
const [showDeleteDestinationForm, setShowDeleteDestinationForm] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [showEditDestinationPanel, setShowEditDestinationPanel] = useState(false)
const [showUpdateVersionModal, setShowUpdateVersionModal] = useState(false)
const { type, statusName, destination, pipeline, pipelineStatus, pipelineFetcher } =
useDestinationInformation({
id: destinationId,
})
const {
error: pipelineError,
isPending: isPipelineLoading,
isError: isPipelineError,
isSuccess: isPipelineSuccess,
} = pipelineFetcher
const destinationName = destination?.name ?? ''
const {
data: pipelineStatusData,
error: pipelineStatusError,
isPending: isPipelineStatusLoading,
isError: isPipelineStatusError,
@@ -78,7 +67,7 @@ export const DestinationRow = ({
},
{ refetchInterval: STATUS_REFRESH_FREQUENCY_MS }
)
const { getRequestStatus, updatePipelineStatus, setRequestStatus } = usePipelineRequestStatus()
const { getRequestStatus, updatePipelineStatus } = usePipelineRequestStatus()
const requestStatus = pipeline?.id
? getRequestStatus(pipeline.id)
: PipelineStatusRequestStatus.None
@@ -86,9 +75,6 @@ export const DestinationRow = ({
const { mutateAsync: stopPipeline } = useStopPipelineMutation()
const { mutateAsync: deleteDestinationPipeline } = useDeleteDestinationPipelineMutation({})
const pipelineStatus = pipelineStatusData?.status
const statusName = getStatusName(pipelineStatus)
// Fetch table-level replication status to surface errors in list view
const { data: replicationStatusData } = useReplicationPipelineReplicationStatusQuery(
{ projectRef, pipelineId: pipeline?.id },
@@ -177,7 +163,7 @@ export const DestinationRow = ({
<ShimmeringLoader />
) : (
<PipelineStatus
pipelineStatus={pipelineStatusData?.status}
pipelineStatus={pipelineStatus?.status}
error={pipelineStatusError}
isLoading={isPipelineStatusLoading}
isError={isPipelineStatusError}
@@ -218,13 +204,13 @@ export const DestinationRow = ({
</Link>
</Button>
<RowMenu
destinationId={destinationId}
pipeline={pipeline}
pipelineStatus={pipelineStatusData?.status}
pipelineStatus={pipelineStatus?.status}
error={pipelineStatusError}
isLoading={isPipelineStatusLoading}
isError={isPipelineStatusError}
onDeleteClick={() => setShowDeleteDestinationForm(true)}
onEditClick={() => setShowEditDestinationPanel(true)}
hasUpdate={hasUpdate}
onUpdateClick={() => setShowUpdateVersionModal(true)}
/>
@@ -241,20 +227,6 @@ export const DestinationRow = ({
name={destinationName}
/>
<DestinationPanel
visible={showEditDestinationPanel}
type={type}
onClose={() => setShowEditDestinationPanel(false)}
existingDestination={{
sourceId,
destinationId: destinationId,
pipelineId: pipeline?.id,
enabled:
statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED,
statusName,
}}
/>
<UpdateVersionModal
visible={showUpdateVersionModal}
pipeline={pipeline}
@@ -10,6 +10,7 @@ import { useReplicationSourcesQuery } from 'data/replication/sources-query'
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { DOCS_URL } from 'lib/constants'
import { Plus, Search, X } from 'lucide-react'
import { parseAsStringEnum, useQueryState } from 'nuqs'
import { useEffect, useRef, useState } from 'react'
import {
Button,
@@ -28,6 +29,7 @@ import { Input } from 'ui-patterns/DataInputs/Input'
import { REPLICA_STATUS } from '../../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
import { DestinationPanel } from './DestinationPanel/DestinationPanel'
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
import { DestinationRow } from './DestinationRow'
import { EnableReplicationCallout } from './EnableReplicationCallout'
import { PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
@@ -44,9 +46,20 @@ export const Destinations = () => {
const prefetchedRef = useRef(false)
const [filterString, setFilterString] = useState<string>('')
const [showNewDestinationPanel, setShowNewDestinationPanel] = useState(false)
const [statusRefetchInterval, setStatusRefetchInterval] = useState<number | false>(5000)
const [_, setDestinationType] = useQueryState(
'type',
parseAsStringEnum<DestinationType>([
'Read Replica',
'BigQuery',
'Analytics Bucket',
]).withOptions({
history: 'push',
clearOnDefault: true,
})
)
const {
data: databases = [],
error: databasesError,
@@ -175,7 +188,7 @@ export const Destinations = () => {
<Button
type="default"
icon={<Plus />}
onClick={() => setShowNewDestinationPanel(true)}
onClick={() => setDestinationType('Read Replica')}
>
Add destination
</Button>
@@ -229,33 +242,9 @@ export const Destinations = () => {
)
})}
{filteredDestinations.map((destination) => {
const pipeline = pipelinesData?.pipelines.find(
(p) => p.destination_id === destination.id
)
const type =
'big_query' in destination.config
? 'BigQuery'
: 'iceberg' in destination.config
? 'Analytics Bucket'
: undefined
return (
<DestinationRow
key={destination.id}
sourceId={sourceId}
destinationId={destination.id}
destinationName={destination.name}
type={type}
pipeline={pipeline}
error={pipelinesError}
isLoading={isPipelinesLoading}
isError={isPipelinesError}
isSuccess={isPipelinesSuccess}
/>
)
})}
{filteredDestinations.map((destination) => (
<DestinationRow key={destination.id} destinationId={destination.id} />
))}
{!isLoading &&
filteredDestinations.length === 0 &&
@@ -296,7 +285,7 @@ export const Destinations = () => {
</p>
<Button
icon={<Plus />}
onClick={() => setShowNewDestinationPanel(true)}
onClick={() => setDestinationType('Read Replica')}
className="mt-4"
>
Add destination
@@ -306,11 +295,7 @@ export const Destinations = () => {
)}
</div>
<DestinationPanel
visible={showNewDestinationPanel}
onClose={() => setShowNewDestinationPanel(false)}
onSuccessCreateReadReplica={() => setStatusRefetchInterval(5000)}
/>
<DestinationPanel onSuccessCreateReadReplica={() => setStatusRefetchInterval(5000)} />
</>
)
}
@@ -16,6 +16,7 @@ import {
} from 'ui'
import { Admonition } from 'ui-patterns'
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
import { DocsButton } from '@/components/ui/DocsButton'
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
import { DOCS_URL } from '@/lib/constants'
@@ -86,7 +87,7 @@ export const EnableReplicationCallout = ({
className,
hasAccess,
}: {
type?: string
type?: DestinationType | null
className?: string
hasAccess: boolean
}) => {
@@ -1,6 +1,3 @@
import { ArrowUpCircle, Edit, MoreVertical, Pause, Play, RotateCcw, Trash } from 'lucide-react'
import { toast } from 'sonner'
import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query'
@@ -8,6 +5,9 @@ import { Pipeline } from 'data/replication/pipelines-query'
import { useRestartPipelineHelper } from 'data/replication/restart-pipeline-helper'
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
import { ArrowUpCircle, Edit, MoreVertical, Pause, Play, RotateCcw, Trash } from 'lucide-react'
import { parseAsInteger, useQueryState } from 'nuqs'
import { toast } from 'sonner'
import {
PipelineStatusRequestStatus,
usePipelineRequestStatus,
@@ -22,6 +22,7 @@ import {
DropdownMenuTrigger,
} from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import {
PIPELINE_DISABLE_ALLOWED_FROM,
PIPELINE_ENABLE_ALLOWED_FROM,
@@ -31,31 +32,36 @@ import {
import { PipelineStatusName } from './Replication.constants'
interface RowMenuProps {
destinationId: number
pipeline: Pipeline | undefined
pipelineStatus?: ReplicationPipelineStatusData['status']
error: ResponseError | null
isLoading: boolean
isError: boolean
onEditClick: () => void
onDeleteClick: () => void
hasUpdate?: boolean
onDeleteClick: () => void
onUpdateClick?: () => void
}
export const RowMenu = ({
destinationId,
pipeline,
pipelineStatus,
error,
isLoading,
isError,
onEditClick,
onDeleteClick,
hasUpdate = false,
onDeleteClick,
onUpdateClick,
}: RowMenuProps) => {
const { ref: projectRef } = useParams()
const statusName = getStatusName(pipelineStatus)
const [_, setEdit] = useQueryState(
'edit',
parseAsInteger.withOptions({ history: 'push', clearOnDefault: true })
)
const { mutateAsync: startPipeline } = useStartPipelineMutation()
const { mutateAsync: stopPipeline } = useStopPipelineMutation()
const { restartPipeline } = useRestartPipelineHelper()
@@ -176,7 +182,7 @@ export const RowMenu = ({
</>
)}
<DropdownMenuItem className="space-x-2" onClick={onEditClick}>
<DropdownMenuItem className="space-x-2" onClick={() => setEdit(destinationId)}>
<Edit size={14} />
<p>Edit destination</p>
</DropdownMenuItem>
@@ -0,0 +1,75 @@
import { useParams } from 'common'
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
import { getStatusName } from './Pipeline.utils'
import { useReplicationDestinationByIdQuery } from '@/data/replication/destination-by-id-query'
import { useReplicationPipelineStatusQuery } from '@/data/replication/pipeline-status-query'
import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query'
import { useReplicationSourcesQuery } from '@/data/replication/sources-query'
export const useDestinationInformation = ({ id }: { id?: number | null }) => {
const { ref: projectRef } = useParams()
const { data: sourcesData, isSuccess: isSourcesSuccess } = useReplicationSourcesQuery({
projectRef,
})
const sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id
const replicationNotEnabled = isSourcesSuccess && !sourceId
const {
data: destination,
error: destinationError,
isPending: isDestinationPending,
isError: isDestinationError,
isSuccess: isDestinationSuccess,
} = useReplicationDestinationByIdQuery({
projectRef,
destinationId: id,
})
const destinationType: DestinationType | undefined = !destination
? undefined
: 'big_query' in destination.config
? 'BigQuery'
: 'iceberg' in destination.config
? 'Analytics Bucket'
: undefined
const {
data: pipelinesData,
error: pipelineError,
isPending: isPipelinePending,
isError: isPipelineError,
isSuccess: isPipelineSuccess,
} = useReplicationPipelinesQuery({ projectRef })
const pipeline = pipelinesData?.pipelines.find((p) => p.destination_id === id)
const { data: pipelineStatus } = useReplicationPipelineStatusQuery({
projectRef,
pipelineId: pipeline?.id,
})
const statusName = getStatusName(pipelineStatus?.status)
return {
sourceId,
destination,
pipeline,
pipelineStatus,
// Derivatives
statusName,
type: destinationType,
replicationNotEnabled,
// Data fetching status (Secondary information)
pipelineFetcher: {
error: pipelineError,
isPending: isPipelinePending,
isError: isPipelineError,
isSuccess: isPipelineSuccess,
},
destinationFetcher: {
error: destinationError,
isPending: isDestinationPending,
isError: isDestinationError,
isSuccess: isDestinationSuccess,
},
}
}
@@ -1,5 +1,8 @@
import { useQueryState, parseAsBoolean } from 'nuqs'
import { parseAsBoolean, useQueryState } from 'nuqs'
/**
* @deprecated Can remove after read replicas is fully moved into database replication page
*/
export function useShowNewReplicaPanel() {
const [showNewReplicaPanel, setShowNewReplicaPanel] = useQueryState(
'createReplica',
+15 -11
View File
@@ -1,11 +1,4 @@
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 { useParams } from 'common'
import { useFlag, useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { REPLICA_STATUS } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
import { useShowNewReplicaPanel } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/use-show-new-replica'
@@ -14,6 +7,12 @@ import { formatDatabaseID, formatDatabaseRegion } from 'data/read-replicas/repli
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { IS_PLATFORM } from 'lib/constants'
import { timeout } from 'lib/helpers'
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 { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
Button,
@@ -57,6 +56,7 @@ export const DatabaseSelector = ({
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const { setShowNewReplicaPanel } = useShowNewReplicaPanel()
const unifiedReplication = useFlag('unifiedReplication')
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
const state = useDatabaseSelectorStateSnapshot()
@@ -74,6 +74,10 @@ export const DatabaseSelector = ({
const selectedAdditionalOption = additionalOptions.find((x) => x.id === selectedDatabaseId)
const newReplicaURL = unifiedReplication
? `/project/${projectRef}/database/replication?type=Read+Replica`
: `/project/${projectRef}/settings/infrastructure`
useEffect(() => {
if (_selectedDatabaseId) state.setSelectedDatabaseId(_selectedDatabaseId)
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -215,12 +219,12 @@ export const DatabaseSelector = ({
className="cursor-pointer w-full"
onSelect={() => {
setOpen(false)
router.push(`/project/${projectRef}/settings/infrastructure`)
router.push(newReplicaURL)
}}
onClick={() => setOpen(false)}
>
<Link
href={`/project/${projectRef}/settings/infrastructure`}
href={newReplicaURL}
onClick={async () => {
setOpen(false)
// [Joshen] This is used in the Connect UI which is available across all pages
@@ -230,7 +234,7 @@ export const DatabaseSelector = ({
// the replica panel from a "portal" based component (e.g dialog, sheet, dropdown, etc)
// Although I'd prefer if there's a better way to resolve this
await timeout(50)
setShowNewReplicaPanel(true)
if (!unifiedReplication) setShowNewReplicaPanel(true)
}}
className="w-full flex items-center gap-2"
>
@@ -1,11 +1,11 @@
import { useQuery } from '@tanstack/react-query'
import type { components } from 'api-types'
import { get, handleError } from 'data/fetchers'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { replicationKeys } from './keys'
type ReplicationDestinationByIdParams = { projectRef?: string; destinationId?: number }
type ReplicationDestinationByIdParams = { projectRef?: string; destinationId?: number | null }
async function fetchReplicationDestinationById(
{ projectRef, destinationId }: ReplicationDestinationByIdParams,
@@ -36,6 +36,10 @@ export const useReplicationDestinationByIdQuery = <TData = ReplicationDestinatio
useQuery<ReplicationDestinationByIdData, ResponseError, TData>({
queryKey: replicationKeys.destinationById(projectRef, destinationId),
queryFn: ({ signal }) => fetchReplicationDestinationById({ projectRef, destinationId }, signal),
enabled: enabled && typeof projectRef !== 'undefined' && typeof destinationId !== 'undefined',
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof destinationId !== 'undefined' &&
destinationId !== null,
...options,
})
+1 -1
View File
@@ -2,7 +2,7 @@ export const replicationKeys = {
sources: (projectRef: string | undefined) => ['projects', projectRef, 'sources'] as const,
destinations: (projectRef: string | undefined) =>
['projects', projectRef, 'destinations'] as const,
destinationById: (projectRef: string | undefined, destinationId: number | undefined) =>
destinationById: (projectRef: string | undefined, destinationId: number | undefined | null) =>
['projects', projectRef, 'destinations', destinationId] as const,
publications: (projectRef: string | undefined, source_id: number | undefined) =>
['projects', projectRef, 'sources', source_id, 'publications'] as const,