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 && ( + + + +
)} 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={} /> - @@ -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, {