From 5e4cc212aa96d4cc3f5ffa7a688ea673f5c5da9b Mon Sep 17 00:00:00 2001 From: Riccardo Busetti Date: Thu, 4 Sep 2025 15:47:01 +0200 Subject: [PATCH] feat(replication): Add ability to copy pipeline id and filter logs by pipeline id (#38407) --- .../Database/Replication/DestinationPanel.tsx | 11 +- .../Database/Replication/DestinationRow.tsx | 20 ++- .../Database/Replication/Pipeline.utils.ts | 3 +- .../Database/Replication/PipelineStatus.tsx | 15 +- .../Replication/ReplicationPipelineStatus.tsx | 142 +++++++++++------- .../Database/Replication/RowMenu.tsx | 102 +++++++++---- .../Settings/Logs/Logs.constants.ts | 4 + .../update-destination-pipeline-mutation.ts | 6 +- 8 files changed, 209 insertions(+), 94 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx index 68199c42788..2d4aea160f4 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx @@ -74,6 +74,7 @@ interface DestinationPanelProps { destinationId: number pipelineId?: number enabled: boolean + statusName?: string } } @@ -178,7 +179,8 @@ export const DestinationPanel = ({ sourceId, }) // Set request status only right before starting, then fire and close - const snapshot = existingDestination.enabled ? 'started' : 'stopped' + const snapshot = + existingDestination.statusName ?? (existingDestination.enabled ? 'started' : 'stopped') if (existingDestination.enabled) { setRequestStatus( existingDestination.pipelineId, @@ -244,6 +246,13 @@ export const DestinationPanel = ({ } }, [destinationData, pipelineData, editMode, defaultValues, form]) + // Ensure the form always reflects the freshest data whenever the panel opens + useEffect(() => { + if (visible) { + form.reset(defaultValues) + } + }, [visible, defaultValues, form]) + return sourceId ? ( <> diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx index 16437c30b5b..28da065f287 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx @@ -125,7 +125,20 @@ export const DestinationRow = ({ )} {isPipelineSuccess && ( - {isPipelineLoading ? : destinationName} + + {isPipelineLoading ? ( + + ) : pipeline?.id ? ( + + + {destinationName} + + Pipeline ID: {pipeline.id} + + ) : ( + destinationName + )} + {isPipelineLoading ? : type} {isPipelineLoading || !pipeline ? ( @@ -138,6 +151,7 @@ export const DestinationRow = ({ isError={isPipelineStatusError} isSuccess={isPipelineStatusSuccess} requestStatus={requestStatus} + pipelineId={pipeline?.id} /> )} @@ -195,7 +209,9 @@ export const DestinationRow = ({ sourceId, destinationId: destinationId, pipelineId: pipeline?.id, - enabled: statusName === PipelineStatusName.STARTED, + enabled: + statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED, + statusName, }} /> diff --git a/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts b/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts index eb69b88e689..8a4733175b7 100644 --- a/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts +++ b/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts @@ -1,5 +1,6 @@ import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query' import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status' +import { PipelineStatusName } from './PipelineStatus' export const PIPELINE_ERROR_MESSAGES = { RETRIEVE_PIPELINE: 'Failed to retrieve pipeline information', @@ -24,7 +25,7 @@ export const getStatusName = ( export const PIPELINE_ENABLE_ALLOWED_FROM = ['stopped'] as const export const PIPELINE_DISABLE_ALLOWED_FROM = ['started', 'failed'] as const -export const PIPELINE_ACTIONABLE_STATES = ['failed', 'started', 'stopped'] as const +export const PIPELINE_ACTIONABLE_STATES = ['failed', 'started', 'stopped'] as PipelineStatusName[] const PIPELINE_STATE_MESSAGES = { enabling: { diff --git a/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx index 14e8b9a9969..e0804c42dfa 100644 --- a/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx @@ -1,7 +1,8 @@ +import { AlertTriangle, Loader2 } from 'lucide-react' + import { useParams } from 'common' import { InlineLink } from 'components/ui/InlineLink' import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query' -import { AlertTriangle, Loader2 } from 'lucide-react' import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status' import { ResponseError } from 'types' import { cn, Tooltip, TooltipContent, TooltipTrigger, WarningIcon } from 'ui' @@ -23,6 +24,7 @@ interface PipelineStatusProps { isError: boolean isSuccess: boolean requestStatus?: PipelineStatusRequestStatus + pipelineId?: number } export const PipelineStatus = ({ @@ -32,6 +34,7 @@ export const PipelineStatus = ({ isError, isSuccess, requestStatus, + pipelineId, }: PipelineStatusProps) => { const { ref } = useParams() @@ -129,6 +132,12 @@ export const PipelineStatus = ({ const statusConfig = getStatusConfig() + const pipelineLogsUrl = pipelineId + ? `/project/${ref}/logs/etl-replication-logs?f=${encodeURIComponent( + JSON.stringify({ pipeline_id: pipelineId }) + )}` + : `/project/${ref}/logs/etl-replication-logs` + return ( <> {isLoading && } @@ -155,9 +164,7 @@ export const PipelineStatus = ({ {['unknown', 'failed'].includes(pipelineStatus?.name ?? '') && ( <> {' '} - Check the{' '} - logs for - more information. + Check the logs for more information. )} diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx index c3e4ca8117e..0bf7a556003 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx @@ -1,4 +1,14 @@ -import { Activity, ChevronLeft, ExternalLink, Search, X } from 'lucide-react' +import { + Activity, + Ban, + ChevronLeft, + ExternalLink, + Pause, + Play, + RotateCcw, + Search, + X, +} from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { toast } from 'sonner' @@ -22,12 +32,10 @@ import { Input } from 'ui-patterns/DataInputs/Input' import { ErroredTableDetails } from './ErroredTableDetails' import { PIPELINE_ACTIONABLE_STATES, - PIPELINE_DISABLE_ALLOWED_FROM, - PIPELINE_ENABLE_ALLOWED_FROM, PIPELINE_ERROR_MESSAGES, getStatusName, } from './Pipeline.utils' -import { PipelineStatus } from './PipelineStatus' +import { PipelineStatus, PipelineStatusName } from './PipelineStatus' import { STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants' import { TableState } from './ReplicationPipelineStatus.types' import { getDisabledStateConfig, getStatusConfig } from './ReplicationPipelineStatus.utils' @@ -104,21 +112,44 @@ export const ReplicationPipelineStatus = () => { requestStatus === PipelineStatusRequestStatus.RestartRequested const showDisabledState = !isPipelineRunning || isEnablingDisabling - const onTogglePipeline = async () => { - if (!projectRef) { - return console.error('Project ref is required') - } - if (!pipeline) { - return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) - } + const logsUrl = `/project/${projectRef}/logs/etl-replication-logs${ + pipelineId ? `?f=${encodeURIComponent(JSON.stringify({ pipeline_id: pipelineId }))}` : '' + }` + + const label = + statusName === 'stopped' + ? 'Start' + : statusName === 'started' + ? 'Stop' + : statusName === 'failed' + ? 'Restart' + : 'Action unavailable' + + const icon = + statusName === 'stopped' ? ( + + ) : statusName === 'started' ? ( + + ) : statusName === 'failed' ? ( + + ) : ( + + ) + + const onPrimaryAction = async () => { + if (!projectRef) return console.error('Project ref is required') + if (!pipeline) return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) try { - if (PIPELINE_ENABLE_ALLOWED_FROM.includes(statusName as any)) { + if (statusName === 'stopped') { setRequestStatus(pipeline.id, PipelineStatusRequestStatus.StartRequested, statusName) await startPipeline({ projectRef, pipelineId: pipeline.id }) - } else if (PIPELINE_DISABLE_ALLOWED_FROM.includes(statusName as any)) { + } else if (statusName === 'started') { setRequestStatus(pipeline.id, PipelineStatusRequestStatus.StopRequested, statusName) await stopPipeline({ projectRef, pipelineId: pipeline.id }) + } else if (statusName === 'failed') { + setRequestStatus(pipeline.id, PipelineStatusRequestStatus.RestartRequested, statusName) + await startPipeline({ projectRef, pipelineId: pipeline.id }) } } catch (error) { toast.error(PIPELINE_ERROR_MESSAGES.ENABLE_DESTINATION) @@ -136,57 +167,57 @@ export const ReplicationPipelineStatus = () => { -
-
-

{destinationName || 'Pipeline'}

- -
+
+

{destinationName || 'Pipeline'}

+
-
- - setFilterString(e.target.value)} - actions={ - filterString.length > 0 - ? [ - setFilterString('')} - />, - ] - : undefined - } - /> -
+ } + className="pl-7 h-[26px] text-xs" + placeholder="Search for tables" + value={filterString} + disabled={isPipelineError} + onChange={(e) => setFilterString(e.target.value)} + actions={ + filterString.length > 0 + ? [ + setFilterString('')} + />, + ] + : undefined + } + /> + +
@@ -231,6 +262,7 @@ export const ReplicationPipelineStatus = () => { )}
+ {/* [Joshen] Should update to use new Table components next time */} Table, diff --git a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx index 7e083058ed5..203aa40bae6 100644 --- a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx +++ b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx @@ -1,8 +1,9 @@ -import { Edit, MoreVertical, Pause, Play, Trash } from 'lucide-react' +import { Edit, MoreVertical, Pause, Play, RotateCcw, Trash } from 'lucide-react' import { toast } from 'sonner' import { useParams } from 'common' import AlertError from 'components/ui/AlertError' +import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query' import { Pipeline } from 'data/replication/pipelines-query' import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation' import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation' @@ -21,6 +22,7 @@ import { } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { + PIPELINE_ACTIONABLE_STATES, PIPELINE_DISABLE_ALLOWED_FROM, PIPELINE_ENABLE_ALLOWED_FROM, PIPELINE_ERROR_MESSAGES, @@ -30,7 +32,7 @@ import { PipelineStatusName } from './PipelineStatus' interface RowMenuProps { pipeline: Pipeline | undefined - pipelineStatus: any + pipelineStatus?: ReplicationPipelineStatusData['status'] error: ResponseError | null isLoading: boolean isError: boolean @@ -48,21 +50,42 @@ export const RowMenu = ({ onDeleteClick, }: RowMenuProps) => { const { ref: projectRef } = useParams() - const statusName = getStatusName(pipelineStatus) - const pipelineEnabled = statusName !== PipelineStatusName.STOPPED const { mutateAsync: startPipeline } = useStartPipelineMutation() const { mutateAsync: stopPipeline } = useStopPipelineMutation() - const { setRequestStatus: setGlobalRequestStatus } = usePipelineRequestStatus() + const { getRequestStatus, setRequestStatus: setGlobalRequestStatus } = usePipelineRequestStatus() + const requestStatus = pipeline?.id + ? getRequestStatus(pipeline.id) + : PipelineStatusRequestStatus.None + + const hasPipelineAction = + requestStatus === PipelineStatusRequestStatus.None && + [PipelineStatusName.STOPPED, PipelineStatusName.STARTED, PipelineStatusName.FAILED].includes( + statusName as PipelineStatusName + ) + + const pipelineActionIcon = + statusName === PipelineStatusName.STOPPED ? ( + + ) : statusName === PipelineStatusName.STARTED ? ( + + ) : statusName === PipelineStatusName.FAILED ? ( + + ) : null + + const pipelineActionLabel = + statusName === PipelineStatusName.STOPPED + ? 'Start pipeline' + : statusName === PipelineStatusName.STARTED + ? 'Stop pipeline' + : statusName === PipelineStatusName.FAILED + ? 'Restart pipeline' + : null const onEnablePipeline = async () => { - if (!projectRef) { - return console.error('Project ref is required') - } - if (!pipeline) { - return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) - } + if (!projectRef) return console.error('Project ref is required') + if (!pipeline) return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) try { // Only show 'enabling' when transitioning from allowed states @@ -77,14 +100,8 @@ export const RowMenu = ({ } const onDisablePipeline = async () => { - if (!projectRef) { - console.error('Project ref is required') - return - } - if (!pipeline) { - toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) - return - } + if (!projectRef) return console.error('Project ref is required') + if (!pipeline) return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) try { // Only show 'disabling' when transitioning from allowed states @@ -98,9 +115,23 @@ export const RowMenu = ({ } } + const onRestartPipeline = async () => { + if (!projectRef) return console.error('Project ref is required') + if (!pipeline) return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) + + try { + setGlobalRequestStatus(pipeline.id, PipelineStatusRequestStatus.RestartRequested, statusName) + await startPipeline({ projectRef, pipelineId: pipeline.id }) + } catch (error) { + setGlobalRequestStatus(pipeline.id, PipelineStatusRequestStatus.None) + toast.error(PIPELINE_ERROR_MESSAGES.ENABLE_DESTINATION) + } + } + return (
{isLoading && } + {isError && ( )} @@ -109,19 +140,30 @@ export const RowMenu = ({