import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import { MoreVertical, Plus, Search, X } from 'lucide-react' import { parseAsStringEnum, useQueryState } from 'nuqs' import { useEffect, useMemo, useRef, useState } from 'react' import { Button, Card, CardContent, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns' 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 { DisableExternalReplicationDialog } from './DisableExternalReplicationDialog' import { ReadReplicaRow } from './ReadReplicas/ReadReplicaRow' import { useIsETLBigQueryPrivateAlpha, useIsETLDucklakePrivateAlpha, useIsETLIcebergPrivateAlpha, useIsETLSnowflakePrivateAlpha, } from './useIsETLPrivateAlpha' import { AlertError } from '@/components/ui/AlertError' import { DocsButton } from '@/components/ui/DocsButton' import { Shortcut } from '@/components/ui/Shortcut' import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query' import { replicationKeys } from '@/data/replication/keys' import { fetchReplicationPipelineVersion } from '@/data/replication/pipeline-version-query' import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query' import { useReplicationSourcesQuery } from '@/data/replication/sources-query' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { DOCS_URL } from '@/lib/constants' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' export const Destinations = () => { const queryClient = useQueryClient() const { ref: projectRef } = useParams() const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha() const etlEnableIceberg = useIsETLIcebergPrivateAlpha() const etlEnableDucklake = useIsETLDucklakePrivateAlpha() const etlEnableSnowflake = useIsETLSnowflakePrivateAlpha() const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas']) const newDestinationDefaultType = infrastructureReadReplicas ? 'Read Replica' : etlEnableBigQuery ? 'BigQuery' : etlEnableIceberg ? 'Analytics Bucket' : etlEnableDucklake ? 'DuckLake' : etlEnableSnowflake ? 'Snowflake' : null const prefetchedRef = useRef(false) const searchInputRef = useRef(null) const [filterString, setFilterString] = useState('') const [statusRefetchInterval, setStatusRefetchInterval] = useState(5000) const [showDisableExternalReplicationDialog, setShowDisableExternalReplicationDialog] = useState(false) const [_, setDestinationType] = useQueryState( 'destinationType', parseAsStringEnum([ 'Read Replica', 'BigQuery', 'Analytics Bucket', 'DuckLake', 'Snowflake', ]).withOptions({ history: 'push', clearOnDefault: true, }) ) const { data: databases = [], error: databasesError, isPending: isDatabasesLoading, isError: isDatabasesError, isSuccess: isDatabasesSuccess, } = useReadReplicasQuery({ projectRef }, { refetchInterval: statusRefetchInterval }) const readReplicas = databases.filter((x) => x.identifier !== projectRef) const hasReplicas = isDatabasesSuccess && readReplicas.length > 0 const filteredReplicas = filterString.length === 0 ? readReplicas : readReplicas.filter((replica) => replica.identifier.includes(filterString.toLowerCase())) const { data: destinationsData, error: destinationsError, isPending: isDestinationsLoading, isError: isDestinationsError, isSuccess: isDestinationsSuccess, } = useReplicationDestinationsQuery({ projectRef, }) const destinations = destinationsData?.destinations ?? [] const hasDestinations = isDestinationsSuccess && destinationsData?.destinations.length > 0 const filteredDestinations = filterString.length === 0 ? (destinations ?? []) : (destinations ?? []).filter((destination) => destination.name.toLowerCase().includes(filterString.toLowerCase()) ) const { data: pipelinesData, isSuccess: isPipelinesSuccess } = useReplicationPipelinesQuery({ projectRef, }) const pipelines = pipelinesData?.pipelines ?? [] const { data: sourcesData, isSuccess: isSourcesSuccess } = useReplicationSourcesQuery({ projectRef, }) const externalReplicationSource = useMemo( () => sourcesData?.sources.find((source) => source.name === projectRef), [projectRef, sourcesData?.sources] ) const canDisableExternalReplication = isSourcesSuccess && isDestinationsSuccess && isPipelinesSuccess && !!externalReplicationSource && destinations.length === 0 && pipelines.length === 0 const isLoading = isDestinationsLoading || isDatabasesLoading const hasErrorsFetchingData = isDestinationsError || isDatabasesError const openDestinationPanel = () => { if (!newDestinationDefaultType) return setDestinationType(newDestinationDefaultType) } useShortcut( SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH, () => { searchInputRef.current?.focus() searchInputRef.current?.select() }, { label: 'Search destinations' } ) useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => setFilterString('')) useEffect(() => { if ( projectRef && !prefetchedRef.current && pipelinesData?.pipelines && pipelinesData.pipelines.length > 0 && isPipelinesSuccess ) { prefetchedRef.current = true pipelinesData.pipelines.forEach((p) => { if (!p?.id) return queryClient.prefetchQuery({ queryKey: replicationKeys.pipelinesVersion(projectRef, p.id), queryFn: ({ signal }) => fetchReplicationPipelineVersion({ projectRef, pipelineId: p.id }, signal), staleTime: Infinity, }) }) } }, [projectRef, pipelinesData?.pipelines, isPipelinesSuccess, queryClient]) useEffect(() => { if (!isDatabasesSuccess) return const pollReplicas = async () => { const fixedStatuses = [ REPLICA_STATUS.ACTIVE_HEALTHY, REPLICA_STATUS.ACTIVE_UNHEALTHY, REPLICA_STATUS.INIT_READ_REPLICA_FAILED, ] const replicasInTransition = readReplicas.filter((db) => !fixedStatuses.includes(db.status)) const hasTransientStatus = replicasInTransition.length > 0 // If all replicas are active healthy, stop fetching statuses if (!hasTransientStatus) setStatusRefetchInterval(false) } pollReplicas() }, [isDatabasesSuccess, readReplicas]) return ( <>
} value={filterString} className="w-full lg:w-52" onChange={(e) => setFilterString(e.target.value)} actions={ filterString.length > 0 && (
{canDisableExternalReplication && (
{hasErrorsFetchingData && ( )} {isLoading ? ( ) : hasReplicas || hasDestinations ? ( Name Status Lag Publication {filteredReplicas.map((replica) => { return ( setStatusRefetchInterval(5000)} /> ) })} {filteredDestinations.map((destination) => ( ))} {!isLoading && filteredDestinations.length === 0 && filteredReplicas.length === 0 && (hasReplicas || hasDestinations) && (

No results found

Your search for "{filterString}" did not return any results.

)}
) : ( !isLoading && !hasErrorsFetchingData && (

Replication keeps your data in sync across systems

Deploy read replicas for lower latency and better resource management, or capture database changes to external destinations for real-time data pipelines.

) )}
setStatusRefetchInterval(5000)} /> ) }