import { useParams } from 'common' import { ChevronRight, Minus } from 'lucide-react' import { useRouter } from 'next/router' import { useState } from 'react' import { toast } from 'sonner' import { cn, TableCell, TableRow } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { DeleteDestination } from './DeleteDestination' import { DestinationLogo } from './DestinationLogo' import { DetailSubtext } from './DetailSubtext' import { PipelineStatePill } from './PipelineStatePill' import { PipelineStatusName } from './Replication.constants' import { getFormattedLagValue, getInitialSyncProgress, } from './ReplicationPipelineStatus/ReplicationPipelineStatus.utils' import { RowMenu } from './RowMenu' import { UpdateVersionModal } from './UpdateVersionModal' import { useDestinationInformation } from './useDestinationInformation' 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 { useStopPipelineMutation } from '@/data/replication/stop-pipeline-mutation' import { createNavigationHandler } from '@/lib/navigation' import { PipelineStatusRequestStatus, usePipelineRequestStatus, } from '@/state/replication-pipeline-request-status' import { type ResponseError } from '@/types' interface DestinationRowProps { destinationId: number } export const DestinationRow = ({ destinationId }: DestinationRowProps) => { const router = useRouter() const { ref: projectRef } = useParams() const [showDeleteDestinationForm, setShowDeleteDestinationForm] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [showUpdateVersionModal, setShowUpdateVersionModal] = useState(false) const { type, statusName, destination, pipeline, pipelineStatus, pipelineFetcher } = useDestinationInformation({ id: destinationId, }) const { error: pipelineError, isPending: isPipelineLoading, isError: isPipelineError, } = pipelineFetcher const destinationName = destination?.name ?? pipeline?.destination_name ?? '' const { error: pipelineStatusError, isPending: isPipelineStatusLoading, isError: isPipelineStatusError, isSuccess: isPipelineStatusSuccess, } = useReplicationPipelineStatusQuery({ projectRef, pipelineId: pipeline?.id, }) const { getRequestStatus } = usePipelineRequestStatus() const requestStatus = pipeline?.id ? getRequestStatus(pipeline.id) : PipelineStatusRequestStatus.None const { mutateAsync: stopPipeline } = useStopPipelineMutation({ onError: () => {} }) const { mutateAsync: deleteDestinationPipeline } = useDeleteDestinationPipelineMutation({ onError: () => {}, }) // Fetch table-level replication status to surface errors in list view const { data: replicationStatusData, isPending: isReplicationStatusLoading, isError: isReplicationStatusError, } = useReplicationPipelineReplicationStatusQuery({ projectRef, pipelineId: pipeline?.id }, {}) const tableStatuses = replicationStatusData?.table_statuses ?? [] const errorCount = tableStatuses.filter((t) => t.state?.name === 'error').length const applyLag = replicationStatusData?.apply_lag // Show the byte-based slot lag (WAL the destination hasn't confirmed flushing yet). The // time-based flush_lag from pg_stat_replication is routinely NULL for logical slots that are // idle or don't report timed feedback, whereas confirmed_flush_lsn_bytes is always populated. const lagBytes = applyLag?.confirmed_flush_lsn_bytes const lag = getFormattedLagValue('bytes', lagBytes) // The lag figure only covers ongoing changes, so it reads as "Caught up" while an initial copy // is still running. Say what's actually happening instead. const { syncingCount } = getInitialSyncProgress(tableStatuses) const isInitialSyncRunning = syncingCount > 0 const isCaughtUp = lagBytes === 0 // Hide old table errors while an optimistic lifecycle action is displayed. const isPipelineStopped = statusName === PipelineStatusName.STOPPED const isTransitioning = requestStatus !== PipelineStatusRequestStatus.None const hasTableErrors = errorCount > 0 && !isPipelineStopped && !isTransitioning // Check if a newer pipeline version is available (one-time check cached for session) const { data: versionData } = useReplicationPipelineVersionQuery({ projectRef, pipelineId: pipeline?.id, }) const hasUpdate = Boolean(versionData?.new_version) const handleNavigation = pipeline ? createNavigationHandler(`/project/${projectRef}/database/pipelines/${pipeline.id}`, router) : undefined const onDeleteClick = async () => { if (!projectRef) { return console.error('Project ref is required') } if (!pipeline) { return toast.error('No pipeline found') } try { setIsDeleting(true) await stopPipeline({ projectRef, pipelineId: pipeline.id, waitUntilStopped: true }) await deleteDestinationPipeline({ projectRef, destinationId: destinationId, pipelineId: pipeline.id, }) // Close dialog after successful deletion setShowDeleteDestinationForm(false) toast.success(`Deleted pipeline "${destinationName}"`) } catch (error) { toast.error(`Failed to delete pipeline: ${(error as ResponseError).message}`) } finally { setIsDeleting(false) } } // Five distinct states, so early returns rather than a ternary chain. const renderLag = () => { if (isReplicationStatusLoading) return if (isInitialSyncRunning) return Initial sync if (isReplicationStatusError || !applyLag) return ( <> Lag unavailable ) if (isCaughtUp) return Caught up return {lag.display} } return ( <> {isPipelineError && ( )} {(isPipelineLoading || pipeline) && ( {type ? : null} {isPipelineLoading && !destinationName ? ( ) : (

{destinationName || type}

{pipeline && ( #{pipeline.id} · {type} {hasTableErrors && ( <> · {errorCount} table error{errorCount === 1 ? '' : 's'} )} )}
)}
{isPipelineLoading || !pipeline ? ( ) : ( )}
{isReplicationStatusLoading && Loading lag} {renderLag()}
{isPipelineLoading || !pipeline ? ( ) : ( pipeline.config.publication_name )}
{pipeline ? ( <>
event.stopPropagation()} onAuxClick={(event) => event.stopPropagation()} onKeyDown={(event) => event.stopPropagation()} > setShowDeleteDestinationForm(true)} hasUpdate={hasUpdate} onUpdateClick={() => setShowUpdateVersionModal(true)} />
) : ( )}
)} setShowUpdateVersionModal(false)} /> ) }