From 6ffc3fc94c3f7899743ceeecec86bdac9fc11981 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 3 Feb 2026 15:54:21 +0800 Subject: [PATCH] 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 ## 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 --- .../DestinationPanel/DestinationPanel.tsx | 101 +++++++++++------- .../DestinationTypeSelection.tsx | 49 ++++++--- .../Database/Replication/DestinationRow.tsx | 74 ++++--------- .../Database/Replication/Destinations.tsx | 55 ++++------ .../Replication/EnableReplicationCallout.tsx | 3 +- .../Database/Replication/RowMenu.tsx | 22 ++-- .../Replication/useDestinationInformation.ts | 75 +++++++++++++ .../use-show-new-replica.tsx | 5 +- .../studio/components/ui/DatabaseSelector.tsx | 26 +++-- .../replication/destination-by-id-query.ts | 10 +- apps/studio/data/replication/keys.ts | 2 +- 11 files changed, 257 insertions(+), 165 deletions(-) create mode 100644 apps/studio/components/interfaces/Database/Replication/useDestinationInformation.ts diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx index f5fd1d81197..8c2d69d6437 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx @@ -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( - type || (unifiedReplication ? 'Read Replica' : 'BigQuery') + const [urlDestinationType, setDestinationType] = useQueryState( + 'type', + parseAsStringEnum([ + '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 = ({ - + - {selectedType === 'Read Replica' ? ( + {destinationType === 'Read Replica' ? ( onSuccessCreateReadReplica?.()} /> ) : unifiedReplication && !enablePgReplicate ? ( @@ -124,14 +151,14 @@ export const DestinationPanel = ({ ) : ( diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx index ede1bb7b12d..af2328c95e0 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx @@ -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([ + '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 (
@@ -36,8 +49,8 @@ export const DestinationTypeSelection = ({
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')) && ( )} - {((!editMode && etlEnableBigQuery) || (editMode && selectedType === 'BigQuery')) && ( + {((!editMode && etlEnableBigQuery) || (editMode && destinationType === 'BigQuery')) && (
@@ -82,7 +96,8 @@ export const DestinationTypeSelection = ({ )} - {((!editMode && etlEnableIceberg) || (editMode && selectedType === 'Analytics Bucket')) && ( + {((!editMode && etlEnableIceberg) || + (editMode && destinationType === 'Analytics Bucket')) && ( - {selectedType !== 'Read Replica' && enablePgReplicate && ( + {destinationType !== 'Read Replica' && enablePgReplicate && (

Replication is in alpha. Expect rapid changes and possible breaking updates.{' '} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx index 694aa39d9c3..27db4f87cc0 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx @@ -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 = ({ ) : ( setShowDeleteDestinationForm(true)} - onEditClick={() => setShowEditDestinationPanel(true)} hasUpdate={hasUpdate} onUpdateClick={() => setShowUpdateVersionModal(true)} /> @@ -241,20 +227,6 @@ export const DestinationRow = ({ name={destinationName} /> - setShowEditDestinationPanel(false)} - existingDestination={{ - sourceId, - destinationId: destinationId, - pipelineId: pipeline?.id, - enabled: - statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED, - statusName, - }} - /> - { const prefetchedRef = useRef(false) const [filterString, setFilterString] = useState('') - const [showNewDestinationPanel, setShowNewDestinationPanel] = useState(false) const [statusRefetchInterval, setStatusRefetchInterval] = useState(5000) + const [_, setDestinationType] = useQueryState( + 'type', + parseAsStringEnum([ + 'Read Replica', + 'BigQuery', + 'Analytics Bucket', + ]).withOptions({ + history: 'push', + clearOnDefault: true, + }) + ) + const { data: databases = [], error: databasesError, @@ -175,7 +188,7 @@ export const Destinations = () => { @@ -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 ( - - ) - })} + {filteredDestinations.map((destination) => ( + + ))} {!isLoading && filteredDestinations.length === 0 && @@ -296,7 +285,7 @@ export const Destinations = () => {

- setShowNewDestinationPanel(false)} - onSuccessCreateReadReplica={() => setStatusRefetchInterval(5000)} - /> + setStatusRefetchInterval(5000)} /> ) } diff --git a/apps/studio/components/interfaces/Database/Replication/EnableReplicationCallout.tsx b/apps/studio/components/interfaces/Database/Replication/EnableReplicationCallout.tsx index 3517938ee0e..630e0335050 100644 --- a/apps/studio/components/interfaces/Database/Replication/EnableReplicationCallout.tsx +++ b/apps/studio/components/interfaces/Database/Replication/EnableReplicationCallout.tsx @@ -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 }) => { diff --git a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx index 0d40eb57dc8..78fa236088e 100644 --- a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx +++ b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx @@ -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 = ({ )} - + setEdit(destinationId)}>

Edit destination

diff --git a/apps/studio/components/interfaces/Database/Replication/useDestinationInformation.ts b/apps/studio/components/interfaces/Database/Replication/useDestinationInformation.ts new file mode 100644 index 00000000000..746cb7d7835 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/useDestinationInformation.ts @@ -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, + }, + } +} diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/use-show-new-replica.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/use-show-new-replica.tsx index 0213133f084..5b89cceade3 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/use-show-new-replica.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/use-show-new-replica.tsx @@ -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', diff --git a/apps/studio/components/ui/DatabaseSelector.tsx b/apps/studio/components/ui/DatabaseSelector.tsx index 2b45f0dafa7..cc0166f9d85 100644 --- a/apps/studio/components/ui/DatabaseSelector.tsx +++ b/apps/studio/components/ui/DatabaseSelector.tsx @@ -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)} > { 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" > diff --git a/apps/studio/data/replication/destination-by-id-query.ts b/apps/studio/data/replication/destination-by-id-query.ts index 80873804aaf..0fc48cc7471 100644 --- a/apps/studio/data/replication/destination-by-id-query.ts +++ b/apps/studio/data/replication/destination-by-id-query.ts @@ -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 = ({ 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, }) diff --git a/apps/studio/data/replication/keys.ts b/apps/studio/data/replication/keys.ts index ed92186f2af..e8fb182760a 100644 --- a/apps/studio/data/replication/keys.ts +++ b/apps/studio/data/replication/keys.ts @@ -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,