import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import { Database } from 'icons' import { MoreVertical, Plus, Search, X } from 'lucide-react' import Link from 'next/link' import { parseAsStringEnum, useQueryState } from 'nuqs' import { useEffect, useMemo, useRef, useState } from 'react' import { Button, Card, CardContent, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { DestinationPanel } from './DestinationPanel/DestinationPanel' import { DestinationType } from './DestinationPanel/DestinationPanel.types' import { DestinationRow } from './DestinationRow' import { DisablePipelinesDialog } from './DisablePipelinesDialog' import { EnablePipelinesModal } from './EnablePipelinesCallout' import { REPLICA_STATUS } from './Replication.constants' import { useIsETLBigQueryPrivateAlpha, useIsETLClickHousePrivateAlpha, useIsETLDucklakePrivateAlpha, useIsETLIcebergPrivateAlpha, useIsETLSnowflakePrivateAlpha, } from './useIsETLPrivateAlpha' import { ReadReplicaRow } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaRow' import { AlertError } from '@/components/ui/AlertError' import { DocsButton } from '@/components/ui/DocsButton' import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip' 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 { checkLocalETLNotSetUp } from '@/data/replication/utils' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { DOCS_URL } from '@/lib/constants' import { onSearchInputEscape } from '@/lib/keyboard' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' export const Destinations = () => { const queryClient = useQueryClient() const { ref: projectRef } = useParams() const { data: organization } = useSelectedOrganizationQuery() const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha() const etlEnableIceberg = useIsETLIcebergPrivateAlpha() const etlEnableDucklake = useIsETLDucklakePrivateAlpha() const etlEnableSnowflake = useIsETLSnowflakePrivateAlpha() const etlEnableClickHouse = useIsETLClickHousePrivateAlpha() const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas']) const newDestinationDefaultType = infrastructureReadReplicas ? 'Read Replica' : etlEnableBigQuery ? 'BigQuery' : etlEnableIceberg ? 'Analytics Bucket' : etlEnableDucklake ? 'DuckLake' : etlEnableSnowflake ? 'Snowflake' : etlEnableClickHouse ? 'ClickHouse' : null const prefetchedRef = useRef(false) const searchInputRef = useRef(null) const [filterString, setFilterString] = useState('') const [statusRefetchInterval, setStatusRefetchInterval] = useState(5000) const [showEnablePipelinesDialog, setShowEnablePipelinesDialog] = useState(false) const [showDisablePipelinesDialog, setShowDisablePipelinesDialog] = useState(false) const [_, setDestinationType] = useQueryState( 'destinationType', parseAsStringEnum([ 'Read Replica', 'BigQuery', 'Analytics Bucket', 'DuckLake', 'Snowflake', 'ClickHouse', ]).withOptions({ history: 'push', clearOnDefault: true, }) ) const { data: databases = [], error: databasesError, isPending: isDatabasesLoading, isError: isDatabasesError, isSuccess: isDatabasesSuccess, } = useReadReplicasQuery({ projectRef }, { refetchInterval: statusRefetchInterval }) // Memoise so the array reference is stable across renders. Without this // the polling useEffect below has an unstable dep, runs every render, and // its `setStatusRefetchInterval(false)` churn keeps the parent re-rendering // — which trips a latent ref-instability bug in @radix-ui/react-slot // (`composeRefs` is called per render instead of `useComposedRefs`) and // tanks the page with "Maximum update depth exceeded" via the Tooltip // trigger refs. const readReplicas = useMemo( () => databases.filter((x) => x.identifier !== projectRef), [databases, 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 replicationNotEnabled = isSourcesSuccess && !externalReplicationSource const canDisablePipelines = isSourcesSuccess && isDestinationsSuccess && isPipelinesSuccess && !!externalReplicationSource && destinations.length === 0 && pipelines.length === 0 const isLoading = isDestinationsLoading || isDatabasesLoading const isLocalETLNotSetUp = checkLocalETLNotSetUp(destinationsError) const hasErrorsFetchingData = (!isLocalETLNotSetUp && 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)} onKeyDown={onSearchInputEscape(filterString, setFilterString)} actions={ filterString.length > 0 && (
{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 && ( ) )}
setStatusRefetchInterval(5000)} />
) }