import { useParams } from 'common' import { ArrowRight, ArrowUpCircle, CircleStop, Edit, MoreVertical, Play, RotateCcw, Trash, } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { parseAsInteger, useQueryState } from 'nuqs' import { PropsWithChildren, useState, type ReactNode } from 'react' import { toast } from 'sonner' import { BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from 'ui' import { PageBreadcrumbs, PageBreadcrumbsActions } from 'ui-patterns/PageBreadcrumbs' import { PageHeader, PageHeaderAside, PageHeaderDescription, PageHeaderIcon, PageHeaderMeta, PageHeaderSummary, PageHeaderTitle, } from 'ui-patterns/PageHeader' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { DeleteDestination } from './DeleteDestination' import { DestinationLogo } from './DestinationLogo' import { DestinationPanel } from './DestinationPanel/DestinationPanel' import { getPipelineDisplayState, getRestartRequestStatus, getStatusName } from './Pipeline.utils' import { PipelineStatePill } from './PipelineStatePill' import { PipelineStatusName } from './Replication.constants' import { getReplicationDestinationType } from './ReplicationDiagram/Nodes.utils' import { UpdateVersionModal } from './UpdateVersionModal' import { DocsButton } from '@/components/ui/DocsButton' import { useDeleteDestinationPipelineMutation } from '@/data/replication/delete-destination-pipeline-mutation' import { useReplicationDestinationByIdQuery } from '@/data/replication/destination-by-id-query' import { useReplicationPipelineByIdQuery } from '@/data/replication/pipeline-by-id-query' import { useReplicationPipelineStatusQuery } from '@/data/replication/pipeline-status-query' import { useReplicationPipelineVersionQuery } from '@/data/replication/pipeline-version-query' import { useRestartPipelineMutation } from '@/data/replication/restart-pipeline-mutation' import { useStartPipelineMutation } from '@/data/replication/start-pipeline-mutation' import { useStopPipelineMutation } from '@/data/replication/stop-pipeline-mutation' import { DOCS_URL } from '@/lib/constants' import { PipelineStatusRequestStatus, usePipelineRequestStatus, } from '@/state/replication-pipeline-request-status' import { type ResponseError } from '@/types' type LifecycleAction = 'start' | 'stop' | 'restart' const LIFECYCLE_BY_STATUS: Partial< Record > = { [PipelineStatusName.STOPPED]: { label: 'Start', action: 'start', icon: }, [PipelineStatusName.STARTED]: { label: 'Stop', action: 'stop', icon: }, [PipelineStatusName.FAILED]: { label: 'Restart', action: 'restart', icon: }, } export const ReplicationPipelineLayout = ({ children }: PropsWithChildren) => { const { ref: projectRef, pipelineId: pipelineIdParam } = useParams() const pipelineId = Number(pipelineIdParam) const router = useRouter() const [showUpdateVersionModal, setShowUpdateVersionModal] = useState(false) const [showDeleteDestination, setShowDeleteDestination] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [, setEdit] = useQueryState( 'edit', parseAsInteger.withOptions({ history: 'push', clearOnDefault: true }) ) const { getRequestStatus, isRequestPending, runWithRequestStatus } = usePipelineRequestStatus() const requestStatus = getRequestStatus(pipelineId) const isPipelineRequestPending = isRequestPending(pipelineId) const { data: pipeline, error: pipelineError, isPending: isPipelineLoading, } = useReplicationPipelineByIdQuery( { projectRef, pipelineId }, { enabled: Number.isSafeInteger(pipelineId) } ) const { data: pipelineStatusData, error: pipelineStatusError, isLoading: isPipelineStatusLoading, isError: isPipelineStatusError, isSuccess: isPipelineStatusSuccess, } = useReplicationPipelineStatusQuery({ projectRef, pipelineId }, { enabled: !!pipelineId }) const { data: versionData } = useReplicationPipelineVersionQuery({ projectRef, pipelineId: pipeline?.id, }) const { data: destination, isPending: isDestinationLoading } = useReplicationDestinationByIdQuery( { projectRef, destinationId: pipeline?.destination_id, } ) const { mutateAsync: startPipeline, isPending: isStartingPipeline } = useStartPipelineMutation({ onError: () => {}, }) const { mutateAsync: stopPipeline, isPending: isStoppingPipeline } = useStopPipelineMutation({ onError: () => {}, }) const { mutateAsync: restartPipeline, isPending: isRestartingPipeline } = useRestartPipelineMutation() const { mutateAsync: deleteDestinationPipeline } = useDeleteDestinationPipelineMutation({ onError: () => {}, }) const statusName = getStatusName(pipelineStatusData?.status) const displayState = getPipelineDisplayState(requestStatus, statusName) const destinationType = getReplicationDestinationType( destination?.config as Record | undefined ) const isPipelineIdentityLoading = isPipelineLoading || (pipeline !== undefined && isDestinationLoading) const hasUpdate = Boolean(versionData?.new_version) const isTransitioning = requestStatus !== PipelineStatusRequestStatus.None // What the primary button offers for each state it can act on. Anything not listed here (a // pipeline mid-transition, or one in an unknown state) has no action, so the button falls back // to the display state's own label and renders no icon. const lifecycle = LIFECYCLE_BY_STATUS[statusName as PipelineStatusName] const isActionable = lifecycle !== undefined const primaryAction: LifecycleAction | undefined = lifecycle?.action const lifecycleLabel = isTransitioning ? displayState.label : (lifecycle?.label ?? displayState.label) // No icon while Starting/Stopping/Restarting: those states have no entry above, and the // button's loading state carries the transition anyway. const lifecycleIcon = lifecycle?.icon // The overflow menu carries the lifecycle actions the primary button isn't already offering, // so the detail page has the same reach as the row menu on the list without repeating itself. const isRunningOrFailed = statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED const canUseMenuActions = isRunningOrFailed && !isTransitioning && !isPipelineStatusError && !!pipeline const canRestart = canUseMenuActions && !isPipelineRequestPending && primaryAction !== 'restart' const canStop = canUseMenuActions && !isPipelineRequestPending && primaryAction !== 'stop' const onLifecycleAction = async (action?: LifecycleAction) => { const resolvedAction = action ?? primaryAction if (!projectRef || !pipeline || resolvedAction === undefined) return try { if (resolvedAction === 'start') { await runWithRequestStatus(pipeline.id, PipelineStatusRequestStatus.StartRequested, () => startPipeline({ projectRef, pipelineId: pipeline.id }) ) } else if (resolvedAction === 'stop') { await runWithRequestStatus(pipeline.id, PipelineStatusRequestStatus.StopRequested, () => stopPipeline({ projectRef, pipelineId: pipeline.id }) ) } else { await runWithRequestStatus(pipeline.id, getRestartRequestStatus(statusName), () => restartPipeline({ projectRef, pipelineId: pipeline.id }) ) } } catch (error) { toast.error(`Failed to ${resolvedAction} pipeline: ${(error as ResponseError).message}`) } } const onDeleteDestination = async () => { if (!projectRef || !pipeline) return try { setIsDeleting(true) if (statusName !== PipelineStatusName.STOPPED) { await stopPipeline({ projectRef, pipelineId: pipeline.id }) } await deleteDestinationPipeline({ projectRef, destinationId: pipeline.destination_id, pipelineId: pipeline.id, }) setShowDeleteDestination(false) toast.success(`Deleted pipeline "${pipeline.destination_name}"`) router.push(`/project/${projectRef}/database/pipelines`) } catch (error) { toast.error(`Failed to delete pipeline: ${(error as ResponseError).message}`) } finally { setIsDeleting(false) } } const logsUrl = `/project/${projectRef}/logs/replication-logs?f=${encodeURIComponent( JSON.stringify({ pipeline_id: pipelineId }) )}` return (
} > Pipelines {isPipelineLoading ? ( Loading pipeline ) : ( (pipeline?.destination_name ?? 'Pipeline') )} {isPipelineIdentityLoading ? ( ) : destinationType !== undefined ? ( ) : ( )} {isPipelineLoading ? ( Loading pipeline ) : ( (pipeline?.destination_name ?? 'Pipeline') )} · Primary database {isPipelineIdentityLoading ? ( Loading destination ) : ( {destinationType ?? pipeline?.destination_name ?? 'Unknown'} )}
{hasUpdate && ( )}
{children} setShowUpdateVersionModal(false)} />
) }