import { useParams } from 'common' import { Activity, ChevronDown, RotateCcw, Search, X } from 'lucide-react' import Link from 'next/link' import { parseAsString, useQueryState } from 'nuqs' import { useMemo, useState } from 'react' import { Button, Card, CardContent, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Table, TableBody, TableHead, TableHeader, TableHeadSort, TableRow, } from 'ui' import { Admonition } from 'ui-patterns/Admonition' import { Input } from 'ui-patterns/DataInputs/Input' import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionContent, PageSectionMeta, PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' import { GenericTableLoader, ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { BatchRestartDialog } from '../BatchRestartDialog' import { ErrorDetailsDialog } from '../ErrorDetailsDialog' import { getStatusName } from '../Pipeline.utils' import { PipelineStatusName } from '../Replication.constants' import { RestartTableDialog } from '../RestartTableDialog' import { PipelineHealthSection } from './PipelineHealthSection' import { getPipelineStateNotice, getTableStatusEmptyState } from './PipelineOverview.utils' import { getDisabledStateConfig } from './ReplicationPipelineStatus.utils' import { TableReplicationRow } from './TableReplicationRow' import { AlertError } from '@/components/ui/AlertError' import { TableRowNoResults } from '@/components/ui/TableRowNoResults' import { useReplicationPipelineByIdQuery } from '@/data/replication/pipeline-by-id-query' import { useReplicationPipelineReplicationStatusQuery, type ReplicationPipelineTableStatus, } from '@/data/replication/pipeline-replication-status-query' import { useReplicationPipelineStatusQuery } from '@/data/replication/pipeline-status-query' import { onSearchInputEscape } from '@/lib/keyboard' import { PipelineStatusRequestStatus, usePipelineRequestStatus, } from '@/state/replication-pipeline-request-status' type TableSortColumn = 'table' | 'status' type TableSort = `${TableSortColumn}:${'asc' | 'desc'}` const TABLE_STATE_SORT_ORDER: ReplicationPipelineTableStatus['state']['name'][] = [ 'error', 'copying_table', 'copied_table', 'following_wal', 'queued', ] const compareTableStates = ( a: ReplicationPipelineTableStatus['state'], b: ReplicationPipelineTableStatus['state'] ) => TABLE_STATE_SORT_ORDER.indexOf(a.name) - TABLE_STATE_SORT_ORDER.indexOf(b.name) const PipelineOverviewSkeleton = () => ( <> Pipeline health
{Array.from({ length: 5 }, (_, index) => (
))}
Replicated tables ) /** * Component for displaying replication pipeline status and table replication details. * Supports both legacy 'error' state and new 'errored' state with retry policies. */ export const ReplicationPipelineStatus = () => { const { ref: projectRef, pipelineId: _pipelineId } = useParams() const [searchString, setSearchString] = useQueryState('search', parseAsString.withDefault('')) const [showErrorDialog, setShowErrorDialog] = useState(false) const [selectedTableError, setSelectedTableError] = useState<{ tableName: string reason: string solution?: string } | null>(null) const [showRestartDialog, setShowRestartDialog] = useState(false) const [selectedTableForRestart, setSelectedTableForRestart] = useState<{ id: number schema: string name: string } | null>(null) const [showBatchRestartDialog, setShowBatchRestartDialog] = useState(false) const [batchRestartMode, setBatchRestartMode] = useState<'all' | 'errored' | null>(null) const [resettingTableIds, setResettingTableIds] = useState>(new Set()) const pipelineId = Number(_pipelineId) const { getRequestStatus, isRequestPending } = usePipelineRequestStatus() const requestStatus = getRequestStatus(pipelineId) const { data: pipeline, error: pipelineError, isPending: isPipelineLoading, isError: isPipelineError, } = useReplicationPipelineByIdQuery({ projectRef, pipelineId, }) const { data: pipelineStatusData, isPending: isPipelineStatusLoading } = useReplicationPipelineStatusQuery({ projectRef, pipelineId }, { enabled: !!pipelineId }) const { data: replicationStatusData, isPending: isStatusLoading, isError: isStatusError, } = useReplicationPipelineReplicationStatusQuery( { projectRef, pipelineId }, { enabled: !!pipelineId } ) const statusName = getStatusName(pipelineStatusData?.status) const config = getDisabledStateConfig({ requestStatus, statusName }) const tableStatuses = useMemo( () => replicationStatusData?.table_statuses ?? [], [replicationStatusData?.table_statuses] ) const applyLagMetrics = replicationStatusData?.apply_lag const [sort, setSort] = useState('table:asc') const [sortColumn, sortDirection] = sort.split(':') as [TableSortColumn, 'asc' | 'desc'] const getAriaSort = (column: TableSortColumn) => { if (sortColumn !== column) return 'none' return sortDirection === 'asc' ? 'ascending' : 'descending' } const handleSortChange = (column: TableSortColumn) => { if (sortColumn !== column) return setSort(`${column}:asc`) setSort(`${column}:${sortDirection === 'asc' ? 'desc' : 'asc'}`) } const filteredTableStatuses = useMemo(() => { const items = searchString.length === 0 ? [...tableStatuses] : tableStatuses.filter((table) => `${table.schema}.${table.name}`.toLowerCase().includes(searchString.toLowerCase()) ) items.sort((a, b) => { const byName = a.schema.localeCompare(b.schema) || a.name.localeCompare(b.name) const comparison = sortColumn === 'table' ? byName : compareTableStates(a.state, b.state) || byName return sortDirection === 'asc' ? comparison : -comparison }) return items }, [tableStatuses, searchString, sortColumn, sortDirection]) const erroredTables = useMemo( () => tableStatuses.filter((table) => table.state.name === 'error'), [tableStatuses] ) const hasErroredTables = erroredTables.length > 0 const isLoading = isPipelineLoading || isPipelineStatusLoading || isStatusLoading const hasTableData = tableStatuses.length > 0 const isPipelineActionable = statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.STOPPED || statusName === PipelineStatusName.FAILED const hasOptimisticStatus = requestStatus !== PipelineStatusRequestStatus.None const isPipelineBusy = hasOptimisticStatus || isRequestPending(pipelineId) const isAnyTableResetting = resettingTableIds.size > 0 const showDisabledState = isPipelineBusy || !isPipelineActionable const canResetErroredTables = hasErroredTables && !showDisabledState const stateNotice = getPipelineStateNotice({ requestStatus, statusName, tableStatuses }) const isSlotDisconnected = !isStatusError && statusName === PipelineStatusName.STARTED && applyLagMetrics?.active === false const logsUrl = `/project/${projectRef}/logs/replication-logs?f=${encodeURIComponent( JSON.stringify({ pipeline_id: pipelineId }) )}` const emptyState = getTableStatusEmptyState({ isDisabled: showDisabledState, disabledStateConfig: config, statusName, }) return ( <>

{isLoading ? 'Loading pipeline details' : ''}

{isPipelineError && ( )} {isLoading && } {!isLoading && ( {stateNotice !== undefined && ( View logs ) : undefined } /> )} {hasErroredTables && !showDisabledState && ( } disabled={isPipelineBusy || isPipelineError} loading={isPipelineBusy} onClick={() => { setBatchRestartMode('errored') setShowBatchRestartDialog(true) }} > Reset failed tables } /> )} {isSlotDisconnected && ( )} {isStatusError && ( )} )} {!isLoading && !(isStatusError && !hasTableData) && ( Replicated tables {hasTableData && (
} size="tiny" className="text-xs w-52" placeholder="Search tables" value={searchString} disabled={isPipelineError} onChange={(e) => setSearchString(e.target.value)} onKeyDown={onSearchInputEscape(searchString, setSearchString)} actions={ searchString.length > 0 && (
Table Status Details {filteredTableStatuses.length === 0 && ( )} {filteredTableStatuses.map((table) => { const isResetting = resettingTableIds.has(table.id) const isErrorState = table.state.name === 'error' const errorReason = isErrorState && 'reason' in table.state ? table.state.reason : undefined const errorSolution = isErrorState && 'solution' in table.state ? (table.state.solution ?? undefined) : undefined return ( { setSelectedTableForRestart({ id: table.id, schema: table.schema, name: table.name, }) setShowRestartDialog(true) }} onSelectShowError={ isErrorState && errorReason ? () => { setSelectedTableError({ tableName: `${table.schema}.${table.name}`, reason: errorReason, solution: errorSolution, }) setShowErrorDialog(true) } : () => {} } /> ) })}
{filteredTableStatuses.length === 0 && searchString.length > 0 ? `No results found for “${searchString}”` : ''}
)} {!hasTableData && ( )}
)}
{/* Restart Table Confirmation Dialog */} {selectedTableForRestart && ( { setResettingTableIds((current) => new Set(current).add(tableId)) }} onResetComplete={(tableId) => { setResettingTableIds((current) => { const next = new Set(current) next.delete(tableId) return next }) }} /> )} {/* Error Details Dialog */} {selectedTableError && ( )} {/* Batch Restart Dialog */} {batchRestartMode && ( { setResettingTableIds((current) => new Set([...current, ...tableIds])) }} onResetComplete={(tableIds) => { setResettingTableIds((current) => { const next = new Set(current) tableIds.forEach((tableId) => next.delete(tableId)) return next }) }} /> )} ) }