This commit is contained in:
Riccardo Busetti committed 2025-07-22 15:05:51 +02:00
1 parent 943587a5d0
commit 5e0fdd67b1
4 files changed
+79 -141

No files matched your search

@@ -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
)}
</Table.td>
<Table.td className="text-right">
{isPipelineLoading || !pipeline ? (
<div className="w-20 ml-auto">
<ShimmeringLoader></ShimmeringLoader>
</div>
) : (
<Button
type="default"
size="tiny"
onClick={() => onSelectPipeline?.(pipeline.id, destinationName)}
>
View details
</Button>
)}
</Table.td>
<Table.td>
<RowMenu
pipelineStatus={pipelineStatusData?.status}
error={pipelineStatusError}
isLoading={isPipelineStatusLoading}
isError={isPipelineStatusError}
onEnableClick={onEnableClick}
onDisableClick={onDisableClick}
onDeleteClick={() => setShowDeleteDestinationForm(true)}
onEditClick={() => setShowEditDestinationPanel(true)}
></RowMenu>
<div className="flex items-center justify-end gap-1">
{pipeline && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="text"
size="tiny"
icon={<Info className="w-3 h-3" />}
onClick={() => onSelectPipeline?.(pipeline.id, destinationName)}
className="h-auto p-1.5 hover:bg-surface-200"
/>
</TooltipTrigger>
<TooltipContent>
<p>View table replication status</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<RowMenu
pipelineStatus={pipelineStatusData?.status}
error={pipelineStatusError}
isLoading={isPipelineStatusLoading}
isError={isPipelineStatusError}
onEnableClick={onEnableClick}
onDisableClick={onDisableClick}
onDeleteClick={() => setShowDeleteDestinationForm(true)}
onEditClick={() => setShowEditDestinationPanel(true)}
/>
</div>
</Table.td>
</Table.tr>
)}
@@ -97,7 +97,6 @@ const Destinations = ({ onSelectPipeline = noop }: DestinationsProps) => {
<Table.th key="type">Type</Table.th>,
<Table.th key="status">Status</Table.th>,
<Table.th key="publication">Publication</Table.th>,
<Table.th key="details" className="text-right">Details</Table.th>,
<Table.th key="actions"></Table.th>,
]}
body={filteredDestinations.map((destination) => {
@@ -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<string>('')
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: <Clock className="w-4 h-4 text-warning-600" />,
badge: <Badge variant="warning">Queued</Badge>,
description: 'Waiting to start replication',
color: 'text-warning-600'
}
case 'copying_table':
return {
icon: <Loader2 className="w-4 h-4 text-brand-600 animate-spin" />,
badge: <Badge variant="brand">Copying</Badge>,
description: 'Initial data copy in progress',
color: 'text-brand-600'
}
case 'copied_table':
return {
icon: <CheckCircle2 className="w-4 h-4 text-success-600" />,
badge: <Badge variant="success">Copied</Badge>,
description: 'Initial copy completed',
color: 'text-success-600'
}
case 'following_wal':
return {
icon: <Activity className="w-4 h-4 text-success-600" />,
badge: <Badge variant="success">Live</Badge>,
description: `Replicating live changes (${state.lag}ms lag)`,
color: 'text-success-600'
}
case 'error':
return {
icon: <AlertTriangle className="w-4 h-4 text-destructive-600" />,
badge: <Badge variant="destructive">Error</Badge>,
description: state.message,
color: 'text-destructive-600'
}
default:
return {
icon: <AlertTriangle className="w-4 h-4 text-warning-600" />,
badge: <Badge variant="warning">Unknown</Badge>,
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' }}
/>
<div>
<h3 className="text-xl font-semibold">{destinationName || 'Pipeline Status'}</h3>
<div className="flex items-center gap-3">
<h3 className="text-xl font-semibold">{destinationName || 'Pipeline'}</h3>
<PipelineStatus
pipelineStatus={pipelineStatusData?.status}
error={pipelineStatusError}
isLoading={isPipelineStatusLoading}
isError={isPipelineStatusError}
isSuccess={isPipelineStatusSuccess}
requestStatus={PipelineStatusRequestStatus.None}
/>
</div>
<p className="text-sm text-foreground-light">
Table replication status • Pipeline {pipelineId}
Table replication status
</p>
</div>
</div>
@@ -216,14 +220,6 @@ const ReplicationPipelineStatus = ({
onChange={(e) => setFilterString(e.target.value)}
icon={<Search size="14" />}
/>
<Button
type="default"
icon={<RefreshCw className={isRefreshing ? 'animate-spin' : ''} />}
onClick={handleRefresh}
loading={isRefreshing}
>
Refresh
</Button>
</div>
</div>
@@ -236,55 +232,6 @@ const ReplicationPipelineStatus = ({
/>
)}
{/* Summary cards */}
{!isStatusLoading && tableStatuses.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{liveTables.length > 0 && (
<div className="p-4 border border-success-300 bg-success-50 rounded-lg">
<div className="flex items-center gap-3">
<Activity className="w-5 h-5 text-success-600" />
<div>
<div className="text-lg font-semibold text-success-900">{liveTables.length}</div>
<div className="text-sm text-success-700">Live replication</div>
</div>
</div>
</div>
)}
{copyingTables.length > 0 && (
<div className="p-4 border border-brand-300 bg-brand-50 rounded-lg">
<div className="flex items-center gap-3">
<Loader2 className="w-5 h-5 text-brand-600 animate-spin" />
<div>
<div className="text-lg font-semibold text-brand-900">{copyingTables.length}</div>
<div className="text-sm text-brand-700">Copying data</div>
</div>
</div>
</div>
)}
{queuedTables.length > 0 && (
<div className="p-4 border border-warning-300 bg-warning-50 rounded-lg">
<div className="flex items-center gap-3">
<Clock className="w-5 h-5 text-warning-600" />
<div>
<div className="text-lg font-semibold text-warning-900">{queuedTables.length}</div>
<div className="text-sm text-warning-700">Queued</div>
</div>
</div>
</div>
)}
{errorTables.length > 0 && (
<div className="p-4 border border-destructive-300 bg-destructive-50 rounded-lg">
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-destructive-600" />
<div>
<div className="text-lg font-semibold text-destructive-900">{errorTables.length}</div>
<div className="text-sm text-destructive-700">Failed</div>
</div>
</div>
</div>
)}
</div>
)}
{/* Error alert */}
{hasErrors && (
@@ -332,10 +279,7 @@ const ReplicationPipelineStatus = ({
</div>
</Table.td>
<Table.td>
<div className="flex items-center gap-2">
{statusConfig.icon}
{statusConfig.badge}
</div>
{statusConfig.badge}
</Table.td>
<Table.td>
<div className="space-y-1">
@@ -395,9 +339,9 @@ const ReplicationPipelineStatus = ({
The status will appear here once replication begins.
</p>
</div>
<Button type="default" onClick={handleRefresh} loading={isRefreshing}>
Refresh Status
</Button>
<p className="text-xs text-foreground-lighter">
Data refreshes automatically every 2 seconds
</p>
</div>
</div>
)}
@@ -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<ReplicationPipelineReplicationStatusData> {
) {
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<typeof fetchReplicationPipelineReplicationStatus>
>
export const useReplicationPipelineReplicationStatusQuery = <TData = ReplicationPipelineReplicationStatusData>(
{ projectRef, pipelineId }: ReplicationPipelineReplicationStatusParams,
{