| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- import { useParams } from 'common'
- import { AnalyticsBucket, BigQuery, Database } from 'icons'
- import { Minus } from 'lucide-react'
- import Link from 'next/link'
- import { useEffect, useState } from 'react'
- import { toast } from 'sonner'
- import {
- Button,
- TableCell,
- TableRow,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
- WarningIcon,
- } from 'ui'
- import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
- import { DeleteDestination } from './DeleteDestination'
- import { PipelineStatus } from './PipelineStatus'
- import { PipelineStatusName, STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants'
- 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 {
- PipelineStatusRequestStatus,
- usePipelineRequestStatus,
- } from '@/state/replication-pipeline-request-status'
- import { type ResponseError } from '@/types'
- interface DestinationRowProps {
- destinationId: number
- }
- export const DestinationRow = ({ destinationId }: DestinationRowProps) => {
- 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,
- isSuccess: isPipelineSuccess,
- } = pipelineFetcher
- const destinationName = destination?.name ?? ''
- const {
- error: pipelineStatusError,
- isPending: isPipelineStatusLoading,
- isError: isPipelineStatusError,
- isSuccess: isPipelineStatusSuccess,
- } = useReplicationPipelineStatusQuery(
- {
- projectRef,
- pipelineId: pipeline?.id,
- },
- { refetchInterval: STATUS_REFRESH_FREQUENCY_MS }
- )
- const { getRequestStatus, updatePipelineStatus } = usePipelineRequestStatus()
- const requestStatus = pipeline?.id
- ? getRequestStatus(pipeline.id)
- : PipelineStatusRequestStatus.None
- const { mutateAsync: stopPipeline } = useStopPipelineMutation()
- const { mutateAsync: deleteDestinationPipeline } = useDeleteDestinationPipelineMutation({})
- // Fetch table-level replication status to surface errors in list view
- const { data: replicationStatusData } = useReplicationPipelineReplicationStatusQuery(
- { projectRef, pipelineId: pipeline?.id },
- { refetchInterval: STATUS_REFRESH_FREQUENCY_MS }
- )
- const tableStatuses = replicationStatusData?.table_statuses ?? []
- const errorCount = tableStatuses.filter((t) => t.state?.name === 'error').length
- // Only show errors when pipeline is running (not when stopped or restarting)
- const isPipelineStopped = statusName === PipelineStatusName.STOPPED
- const isRestarting = requestStatus === PipelineStatusRequestStatus.RestartRequested
- const hasTableErrors = errorCount > 0 && !isPipelineStopped && !isRestarting
- // 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 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 })
- await deleteDestinationPipeline({
- projectRef,
- destinationId: destinationId,
- pipelineId: pipeline.id,
- })
- // Close dialog after successful deletion
- setShowDeleteDestinationForm(false)
- toast.success(`Deleted destination "${destinationName}"`)
- } catch (error) {
- toast.error(`Failed to delete destination: ${(error as ResponseError).message}`)
- } finally {
- setIsDeleting(false)
- }
- }
- useEffect(() => {
- if (pipeline?.id) {
- updatePipelineStatus(pipeline.id, statusName)
- }
- }, [pipeline?.id, statusName, updatePipelineStatus])
- return (
- <>
- {isPipelineError && (
- <AlertError error={pipelineError} subject="Failed to retrieve pipeline information" />
- )}
- {isPipelineSuccess && (
- <TableRow>
- <TableCell>
- {type === 'BigQuery' ? (
- <BigQuery size={18} className="text-foreground-light" />
- ) : type === 'Analytics Bucket' ? (
- <AnalyticsBucket size={18} className="text-foreground-light" />
- ) : type === 'DuckLake' ? (
- <Database size={18} className="text-foreground-light" />
- ) : (
- <Database size={18} className="text-foreground-light" />
- )}
- </TableCell>
- <TableCell className="max-w-[180px]">
- {isPipelineLoading ? (
- <ShimmeringLoader />
- ) : (
- <div>
- <p>
- {type} (ID: {pipeline?.id})
- </p>
- <p className="text-foreground-lighter">{destinationName}</p>
- </div>
- )}
- </TableCell>
- <TableCell>
- {isPipelineLoading || !pipeline ? (
- <ShimmeringLoader />
- ) : (
- <PipelineStatus
- pipelineStatus={pipelineStatus?.status}
- error={pipelineStatusError}
- isLoading={isPipelineStatusLoading}
- isError={isPipelineStatusError}
- isSuccess={isPipelineStatusSuccess}
- requestStatus={requestStatus}
- pipelineId={pipeline?.id}
- />
- )}
- </TableCell>
- <TableCell>
- <Minus size={18} className="text-foreground-lighter" />
- </TableCell>
- <TableCell>
- {isPipelineLoading || !pipeline ? (
- <ShimmeringLoader />
- ) : (
- pipeline.config.publication_name
- )}
- </TableCell>
- <TableCell>
- <div className="flex items-center justify-end gap-x-2">
- {hasTableErrors && (
- <Tooltip>
- <TooltipTrigger>
- <WarningIcon />
- </TooltipTrigger>
- <TooltipContent side="bottom">
- {errorCount} table{errorCount === 1 ? '' : 's'} encountered replication errors
- </TooltipContent>
- </Tooltip>
- )}
- <Button asChild type="default" className="relative">
- <Link href={`/project/${projectRef}/database/replication/${pipeline?.id}`}>
- View replication
- </Link>
- </Button>
- <RowMenu
- destinationId={destinationId}
- pipeline={pipeline}
- pipelineStatus={pipelineStatus?.status}
- error={pipelineStatusError}
- isLoading={isPipelineStatusLoading}
- isError={isPipelineStatusError}
- onDeleteClick={() => setShowDeleteDestinationForm(true)}
- hasUpdate={hasUpdate}
- onUpdateClick={() => setShowUpdateVersionModal(true)}
- />
- </div>
- </TableCell>
- </TableRow>
- )}
- <DeleteDestination
- visible={showDeleteDestinationForm}
- setVisible={setShowDeleteDestinationForm}
- onDelete={onDeleteClick}
- isLoading={isDeleting}
- name={destinationName}
- />
- <UpdateVersionModal
- visible={showUpdateVersionModal}
- pipeline={pipeline}
- onClose={() => setShowUpdateVersionModal(false)}
- confirmLabel={
- statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED
- ? 'Update and restart'
- : 'Update version'
- }
- />
- </>
- )
- }
|