diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx
index 072dffa922a..1a76c0667e5 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx
@@ -2,7 +2,8 @@ import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { ReplicationPipelinesData } from 'data/replication/pipelines-query'
import { ResponseError } from 'types'
-import { Button } from 'ui'
+import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
+import { Info } from 'lucide-react'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import RowMenu from './RowMenu'
import PipelineStatus, { PipelineStatusRequestStatus, PipelineStatusName } from './PipelineStatus'
@@ -21,7 +22,7 @@ import DestinationPanel from './DestinationPanel'
export type Pipeline = ReplicationPipelinesData['pipelines'][0]
-const refreshFrequencyMs: number = 5000
+const refreshFrequencyMs: number = 2000
interface DestinationRowProps {
sourceId: number | undefined
@@ -179,32 +180,37 @@ const DestinationRow = ({
pipeline.config.publication_name
)}
-
- {isPipelineLoading || !pipeline ? (
-
-
-
- ) : (
-
- )}
-
- setShowDeleteDestinationForm(true)}
- onEditClick={() => setShowEditDestinationPanel(true)}
- >
+
+ {pipeline && (
+
+
+
+ }
+ onClick={() => onSelectPipeline?.(pipeline.id, destinationName)}
+ className="h-auto p-1.5 hover:bg-surface-200"
+ />
+
+
+ View table replication status
+
+
+
+ )}
+
setShowDeleteDestinationForm(true)}
+ onEditClick={() => setShowEditDestinationPanel(true)}
+ />
+
)}
diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
index 0d479c0a74a..6af03f49edb 100644
--- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
@@ -97,7 +97,6 @@ const Destinations = ({ onSelectPipeline = noop }: DestinationsProps) => {
Type,
Status,
Publication,
- Details,
,
]}
body={filteredDestinations.map((destination) => {
diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx
index 84ab8c28e4a..458ce161e42 100644
--- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx
@@ -3,6 +3,8 @@ import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { useReplicationPipelineReplicationStatusQuery } from 'data/replication/pipeline-replication-status-query'
import { useReplicationPipelineByIdQuery } from 'data/replication/pipeline-by-id-query'
+import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
+import PipelineStatus, { PipelineStatusRequestStatus } from './PipelineStatus'
import { useState } from 'react'
import {
Button,
@@ -15,16 +17,12 @@ import {
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import {
- CheckCircle2,
- Clock,
Copy,
- AlertTriangle,
- Loader2,
- Activity,
ExternalLink,
- RefreshCw,
ChevronLeft,
Search,
+ AlertTriangle,
+ Activity,
} from 'lucide-react'
import { toast } from 'sonner'
import { copyToClipboard } from 'ui'
@@ -52,7 +50,6 @@ const ReplicationPipelineStatus = ({
}: ReplicationPipelineStatusProps) => {
const { ref: projectRef } = useParams()
const [filterString, setFilterString] = useState('')
- const [isRefreshing, setIsRefreshing] = useState(false)
const {
data: pipelineData,
@@ -64,25 +61,33 @@ const ReplicationPipelineStatus = ({
pipelineId,
})
+ const {
+ data: pipelineStatusData,
+ error: pipelineStatusError,
+ isLoading: isPipelineStatusLoading,
+ isError: isPipelineStatusError,
+ isSuccess: isPipelineStatusSuccess,
+ } = useReplicationPipelineStatusQuery(
+ { projectRef, pipelineId },
+ {
+ enabled: !!pipelineId,
+ refetchInterval: 2000 // Poll every 2 seconds
+ }
+ )
+
const {
data: replicationStatusData,
error: statusError,
isLoading: isStatusLoading,
isError: isStatusError,
- refetch: refetchStatus,
} = useReplicationPipelineReplicationStatusQuery(
{ projectRef, pipelineId },
{
enabled: !!pipelineId,
- refetchInterval: 5000 // Poll every 5 seconds
+ refetchInterval: 2000 // Poll every 2 seconds
}
)
- const handleRefresh = async () => {
- setIsRefreshing(true)
- await refetchStatus()
- setTimeout(() => setIsRefreshing(false), 1000)
- }
const handleCopyTableStatus = async (tableName: string, state: TableState['state']) => {
const statusText = `Table: ${tableName}\nStatus: ${state.name}${
@@ -103,42 +108,36 @@ const ReplicationPipelineStatus = ({
switch (state.name) {
case 'queued':
return {
- icon: ,
badge: Queued,
description: 'Waiting to start replication',
color: 'text-warning-600'
}
case 'copying_table':
return {
- icon: ,
badge: Copying,
description: 'Initial data copy in progress',
color: 'text-brand-600'
}
case 'copied_table':
return {
- icon: ,
badge: Copied,
description: 'Initial copy completed',
color: 'text-success-600'
}
case 'following_wal':
return {
- icon: ,
badge: Live,
description: `Replicating live changes (${state.lag}ms lag)`,
color: 'text-success-600'
}
case 'error':
return {
- icon: ,
badge: Error,
description: state.message,
color: 'text-destructive-600'
}
default:
return {
- icon: ,
badge: Unknown,
description: 'Unknown status',
color: 'text-warning-600'
@@ -161,11 +160,6 @@ const ReplicationPipelineStatus = ({
)
const errorTables = tableStatuses.filter((table: TableState) => table.state.name === 'error')
- const queuedTables = tableStatuses.filter((table: TableState) => table.state.name === 'queued')
- const copyingTables = tableStatuses.filter((table: TableState) => table.state.name === 'copying_table')
- const liveTables = tableStatuses.filter((table: TableState) => table.state.name === 'following_wal')
- const completedTables = tableStatuses.filter((table: TableState) => table.state.name === 'copied_table')
-
const hasErrors = errorTables.length > 0
if (isPipelineError) {
@@ -202,9 +196,19 @@ const ReplicationPipelineStatus = ({
style={{ padding: '5px' }}
/>
-
{destinationName || 'Pipeline Status'}
+
+
{destinationName || 'Pipeline'}
+
+
- Table replication status • Pipeline {pipelineId}
+ Table replication status
@@ -216,14 +220,6 @@ const ReplicationPipelineStatus = ({
onChange={(e) => setFilterString(e.target.value)}
icon={}
/>
- }
- onClick={handleRefresh}
- loading={isRefreshing}
- >
- Refresh
-
@@ -236,55 +232,6 @@ const ReplicationPipelineStatus = ({
/>
)}
- {/* Summary cards */}
- {!isStatusLoading && tableStatuses.length > 0 && (
-
- {liveTables.length > 0 && (
-
-
-
-
-
{liveTables.length}
-
Live replication
-
-
-
- )}
- {copyingTables.length > 0 && (
-
-
-
-
-
{copyingTables.length}
-
Copying data
-
-
-
- )}
- {queuedTables.length > 0 && (
-
-
-
-
-
{queuedTables.length}
-
Queued
-
-
-
- )}
- {errorTables.length > 0 && (
-
-
-
-
-
{errorTables.length}
-
Failed
-
-
-
- )}
-
- )}
{/* Error alert */}
{hasErrors && (
@@ -332,10 +279,7 @@ const ReplicationPipelineStatus = ({
-
- {statusConfig.icon}
- {statusConfig.badge}
-
+ {statusConfig.badge}
@@ -395,9 +339,9 @@ const ReplicationPipelineStatus = ({
The status will appear here once replication begins.
-
+
+ Data refreshes automatically every 2 seconds
+
)}
diff --git a/apps/studio/data/replication/pipeline-replication-status-query.ts b/apps/studio/data/replication/pipeline-replication-status-query.ts
index 184842bb966..4c1b566fd4f 100644
--- a/apps/studio/data/replication/pipeline-replication-status-query.ts
+++ b/apps/studio/data/replication/pipeline-replication-status-query.ts
@@ -6,39 +6,28 @@ import { replicationKeys } from './keys'
type ReplicationPipelineReplicationStatusParams = { projectRef?: string; pipelineId?: number }
-export type ReplicationPipelineReplicationStatusData = {
- pipeline_id: number
- table_statuses: Array<{
- table_name: string
- state:
- | { name: 'queued' }
- | { name: 'copying_table' }
- | { name: 'copied_table' }
- | { name: 'following_wal'; lag: number }
- | { name: 'error'; message: string }
- }>
-}
-
async function fetchReplicationPipelineReplicationStatus(
{ projectRef, pipelineId }: ReplicationPipelineReplicationStatusParams,
signal?: AbortSignal
-): Promise {
+) {
if (!projectRef) throw new Error('projectRef is required')
if (!pipelineId) throw new Error('pipelineId is required')
- // Temporary implementation - in real usage, this would call the actual API endpoint
- const response = await fetch(`/platform/replication/${projectRef}/pipelines/${pipelineId}/replication-status`, {
+ const { data, error } = await get('/platform/replication/{ref}/pipelines/{pipeline_id}/replication-status', {
+ params: { path: { ref: projectRef, pipeline_id: pipelineId } },
signal,
})
-
- if (!response.ok) {
- throw new Error('Failed to fetch replication status')
+ if (error) {
+ handleError(error)
}
-
- const data = await response.json()
- return data as ReplicationPipelineReplicationStatusData
+
+ return data
}
+export type ReplicationPipelineReplicationStatusData = Awaited<
+ ReturnType
+>
+
export const useReplicationPipelineReplicationStatusQuery = (
{ projectRef, pipelineId }: ReplicationPipelineReplicationStatusParams,
{