diff --git a/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanel.tsx index 7ac88336bc7..ac7291671b0 100644 --- a/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanel.tsx +++ b/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanel.tsx @@ -15,6 +15,7 @@ import { useCreateDestinationPipelineMutation } from 'data/etl/create-destinatio import { useReplicationDestinationByIdQuery } from 'data/etl/destination-by-id-query' import { useReplicationPipelineByIdQuery } from 'data/etl/pipeline-by-id-query' import { useReplicationPublicationsQuery } from 'data/etl/publications-query' +import { useRestartPipelineHelper } from 'data/etl/restart-pipeline-helper' import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation' import { useUpdateDestinationPipelineMutation } from 'data/etl/update-destination-pipeline-mutation' import { useIcebergNamespaceCreateMutation } from 'data/storage/iceberg-namespace-create-mutation' @@ -36,6 +37,7 @@ import { SheetSection, SheetTitle, } from 'ui' +import { NewPublicationPanel } from '../NewPublicationPanel' import { ReplicationDisclaimerDialog } from '../ReplicationDisclaimerDialog' import { AdvancedSettings } from './AdvancedSettings' import { DestinationNameInput } from './DestinationNameInput' @@ -88,6 +90,7 @@ export const DestinationPanel = ({ const editMode = !!existingDestination const [showDisclaimerDialog, setShowDisclaimerDialog] = useState(false) + const [publicationPanelVisible, setPublicationPanelVisible] = useState(false) const [pendingFormValues, setPendingFormValues] = useState | null>( null ) @@ -104,12 +107,13 @@ export const DestinationPanel = ({ }) const { mutateAsync: startPipeline, isPending: startingPipeline } = useStartPipelineMutation() + const { restartPipeline } = useRestartPipelineHelper() const { mutateAsync: createS3AccessKey, isPending: isCreatingS3AccessKey } = useS3AccessKeyCreateMutation() const { mutateAsync: createNamespace, isPending: isCreatingNamespace } = - useIcebergNamespaceCreateMutation() + useIcebergNamespaceCreateMutation({ projectRef }) const { data: publications = [], @@ -317,6 +321,7 @@ export const DestinationPanel = ({ snapshot ) toast.success('Settings applied. Restarting the pipeline...') + restartPipeline({ projectRef, pipelineId: existingDestination.pipelineId }) } else { setRequestStatus( existingDestination.pipelineId, @@ -324,8 +329,8 @@ export const DestinationPanel = ({ snapshot ) toast.success('Settings applied. Starting the pipeline...') + startPipeline({ projectRef, pipelineId: existingDestination.pipelineId }) } - startPipeline({ projectRef, pipelineId: existingDestination.pipelineId }) onClose() } else { let destinationConfig: any = {} @@ -440,7 +445,11 @@ export const DestinationPanel = ({ return ( <> - +
{editMode ? 'Edit destination' : 'Create a new destination'} @@ -467,7 +476,12 @@ export const DestinationPanel = ({
- + setPublicationPanelVisible(true)} + />
@@ -514,6 +528,12 @@ export const DestinationPanel = ({
+ setPublicationPanelVisible(false)} + /> + sourceId?: number visible: boolean + onSelectNewPublication: () => void } -export const PublicationSelection = ({ form, sourceId, visible }: PublicationSelectionProps) => { +export const PublicationSelection = ({ + form, + sourceId, + visible, + onSelectNewPublication, +}: PublicationSelectionProps) => { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() const { publicationName } = form.watch() - const [publicationPanelVisible, setPublicationPanelVisible] = useState(false) - const { data: publications = [], isLoading: isLoadingPublications, @@ -64,7 +67,7 @@ export const PublicationSelection = ({ form, sourceId, visible }: PublicationSel isLoadingPublications={isLoadingPublications} isLoadingCheck={!!selectedPublication && isLoadingCheck} field={field} - onNewPublicationClick={() => setPublicationPanelVisible(true)} + onNewPublicationClick={() => onSelectNewPublication()} /> {isSelectedPublicationMissing ? ( @@ -98,12 +101,6 @@ export const PublicationSelection = ({ form, sourceId, visible }: PublicationSel )} /> - - setPublicationPanelVisible(false)} - /> ) } diff --git a/apps/studio/components/interfaces/Database/ETL/DestinationRow.tsx b/apps/studio/components/interfaces/Database/ETL/DestinationRow.tsx index 4fa6d64f7fd..4c96439ce81 100644 --- a/apps/studio/components/interfaces/Database/ETL/DestinationRow.tsx +++ b/apps/studio/components/interfaces/Database/ETL/DestinationRow.tsx @@ -88,7 +88,10 @@ export const DestinationRow = ({ ) const tableStatuses = replicationStatusData?.table_statuses ?? [] const errorCount = tableStatuses.filter((t) => t.state?.name === 'error').length - const hasTableErrors = errorCount > 0 + // 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({ diff --git a/apps/studio/components/interfaces/Database/ETL/Destinations.tsx b/apps/studio/components/interfaces/Database/ETL/Destinations.tsx index 4a392ad5326..9fb917d22da 100644 --- a/apps/studio/components/interfaces/Database/ETL/Destinations.tsx +++ b/apps/studio/components/interfaces/Database/ETL/Destinations.tsx @@ -113,11 +113,18 @@ export const Destinations = () => { /> - {!!sourceId && ( - - )} +
+ {!!sourceId && ( + + )} + +
diff --git a/apps/studio/components/interfaces/Database/ETL/ErrorDetailsButton.tsx b/apps/studio/components/interfaces/Database/ETL/ErrorDetailsButton.tsx new file mode 100644 index 00000000000..d1213bbffd3 --- /dev/null +++ b/apps/studio/components/interfaces/Database/ETL/ErrorDetailsButton.tsx @@ -0,0 +1,109 @@ +import { AlertCircle } from 'lucide-react' +import { + Button, + cn, + CodeBlock, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, +} from 'ui' + +interface ErrorDetailsButtonProps { + tableName: string + reason: string + solution?: string +} + +export const ErrorDetailsButton = ({ tableName, reason, solution }: ErrorDetailsButtonProps) => { + return ( + + + + + + + Replication error on "{tableName}" + + + +
+

+ The following error occured during replication: +

+
+ pre]:border-x-0 [&>pre]:rounded-none' + )} + language="bash" + value={reason} + className="[&_code]:text-xs [&_code]:text-foreground [&_span]:!text-foreground" + /> + {/* Solution if available */} + {solution && ( +
+

{solution}

+
+ )} +
+ + + + + +
+
+ ) + + return ( + + + + + +
+ {/* Error message */} +
+
Error
+
+
{reason}
+
+
+ + {/* Solution if available */} + {solution && ( +
+
Solution
+

{solution}

+
+ )} +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Database/ETL/ErroredTableDetails.tsx b/apps/studio/components/interfaces/Database/ETL/ErroredTableDetails.tsx index ba0bdad5e4d..666766af7f2 100644 --- a/apps/studio/components/interfaces/Database/ETL/ErroredTableDetails.tsx +++ b/apps/studio/components/interfaces/Database/ETL/ErroredTableDetails.tsx @@ -1,9 +1,10 @@ import { useParams } from 'common' import { InlineLink } from 'components/ui/InlineLink' +import { ErrorDetailsButton } from './ErrorDetailsButton' import { TableState } from './ReplicationPipelineStatus/ReplicationPipelineStatus.types' import { isValidRetryPolicy } from './ReplicationPipelineStatus/ReplicationPipelineStatus.utils' +import { ResetTableButton } from './ResetTableButton' import { RetryCountdown } from './RetryCountdown' -import { RetryOptionsDropdown } from './RetryOptionsDropdown' interface ErroredTableDetailsProps { state: Extract @@ -31,27 +32,52 @@ export const ErroredTableDetails = ({ state, tableName, tableId }: ErroredTableD return (
{retryPolicy === 'no_retry' ? ( -

- This error requires manual intervention from our{' '} - - support - - . Alternatively, you may also recreate the pipeline. -

+
+

+ This error requires manual intervention from our{' '} + + support + + . Alternatively, you may also recreate the pipeline. +

+ +
) : retryPolicy === 'manual_retry' ? ( -
-

{state.solution}. You may thereafter rollback the pipeline.

- +
+
+

+ {state.solution} + {state.solution && !/[.!?]$/.test(state.solution.trim()) && '.'} +

+

You can reset the table to start replication from scratch.

+
+
+ + +
) : retryPolicy === 'timed_retry' ? ( -
+

A retry will be triggered automatically by restarting the pipeline on this table.

+
) : null}
diff --git a/apps/studio/components/interfaces/Database/ETL/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx b/apps/studio/components/interfaces/Database/ETL/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx index 51e34cc0e70..f05aece3cea 100644 --- a/apps/studio/components/interfaces/Database/ETL/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/ETL/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx @@ -25,6 +25,7 @@ import { useReplicationPipelineByIdQuery } from 'data/etl/pipeline-by-id-query' import { useReplicationPipelineReplicationStatusQuery } from 'data/etl/pipeline-replication-status-query' import { useReplicationPipelineStatusQuery } from 'data/etl/pipeline-status-query' import { useReplicationPipelineVersionQuery } from 'data/etl/pipeline-version-query' +import { useRestartPipelineHelper } from 'data/etl/restart-pipeline-helper' import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation' import { useStopPipelineMutation } from 'data/etl/stop-pipeline-mutation' import { @@ -105,6 +106,7 @@ export const ReplicationPipelineStatus = () => { const { mutateAsync: startPipeline, isPending: isStartingPipeline } = useStartPipelineMutation() const { mutateAsync: stopPipeline, isPending: isStoppingPipeline } = useStopPipelineMutation() + const { restartPipeline } = useRestartPipelineHelper() const destinationName = pipeline?.destination_name const statusName = getStatusName(pipelineStatusData?.status) @@ -169,7 +171,7 @@ export const ReplicationPipelineStatus = () => { await stopPipeline({ projectRef, pipelineId: pipeline.id }) } else if (statusName === 'failed') { setRequestStatus(pipeline.id, PipelineStatusRequestStatus.RestartRequested, statusName) - await startPipeline({ projectRef, pipelineId: pipeline.id }) + await restartPipeline({ projectRef, pipelineId: pipeline.id }) } } catch (error) { toast.error(PIPELINE_ERROR_MESSAGES.ENABLE_DESTINATION) @@ -411,7 +413,7 @@ export const ReplicationPipelineStatus = () => { Status unavailable while pipeline is {config.badge.toLowerCase()}

) : ( -
+
{statusConfig.description}
@@ -459,6 +461,7 @@ export const ReplicationPipelineStatus = () => {
)}
+ { + const { ref: projectRef, pipelineId: _pipelineId } = useParams() + const [isOpen, setIsOpen] = useState(false) + + const { resetTable, isRollingBack, isRestartingPipeline, isResetting } = useTableReset({ + tableName, + onSuccess: () => setIsOpen(false), + onError: () => setIsOpen(false), + }) + + const handleReset = () => { + if (!projectRef) return toast.error('Project ref is required') + if (!_pipelineId) return toast.error('Pipeline ID is required') + + const pipelineId = Number(_pipelineId) + + resetTable({ + projectRef, + pipelineId, + tableId, + rollbackType: 'full', + }) + } + + return ( + + + + + Reset and restart table "{tableName}"? + +

+ This will reset and restart replication for this table only. The table will start + copying from scratch, and any existing data for this table downstream will be deleted. +

+

+ Other tables in the pipeline will not be affected. Only this table will be restarted + and go through the full replication process again, starting with the initial copy + phase. +

+

+ The pipeline will be restarted to apply the table reset. +

+
+
+ + Cancel + + {isRollingBack + ? 'Resetting table...' + : isRestartingPipeline + ? 'Restarting pipeline...' + : 'Confirm reset and restart'} + + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Database/ETL/RetryOptionsDropdown.tsx b/apps/studio/components/interfaces/Database/ETL/RetryOptionsDropdown.tsx index 862511fdfa6..90fb4cadf5e 100644 --- a/apps/studio/components/interfaces/Database/ETL/RetryOptionsDropdown.tsx +++ b/apps/studio/components/interfaces/Database/ETL/RetryOptionsDropdown.tsx @@ -3,8 +3,8 @@ import { useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' +import { useRestartPipelineHelper } from 'data/etl/restart-pipeline-helper' import { RollbackType, useRollbackTableMutation } from 'data/etl/rollback-table-mutation' -import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation' import { Button, DropdownMenu, @@ -36,12 +36,27 @@ interface RetryOptionsDropdownProps { export const RetryOptionsDropdown = ({ tableId, tableName }: RetryOptionsDropdownProps) => { const { ref: projectRef, pipelineId: _pipelineId } = useParams() const [isOpen, setIsOpen] = useState(false) + const [isRestartingPipeline, setIsRestartingPipeline] = useState(false) + + const { restartPipeline } = useRestartPipelineHelper() const { mutate: rollbackTable, isPending: isRollingBack } = useRollbackTableMutation({ - onSuccess: (_, vars) => { - const { projectRef, pipelineId } = vars - toast.success(`Table "${tableName}" rolled back successfully and pipeline is being restarted`) - startPipeline({ projectRef, pipelineId }) + onSuccess: async (_, vars) => { + const { projectRef, pipelineId, rollbackType } = vars + toast.success( + `Table "${tableName}" ${rollbackType === 'full' ? 'reset' : 'rolled back'} successfully and pipeline is being restarted` + ) + + setIsRestartingPipeline(true) + try { + await restartPipeline({ projectRef, pipelineId }) + toast.success('Pipeline restarted successfully') + } catch (error: any) { + toast.error(`Failed to restart pipeline: ${error.message}`) + } finally { + setIsRestartingPipeline(false) + setIsOpen(false) + } }, onError: (error, vars) => { const { rollbackType } = vars @@ -50,16 +65,6 @@ export const RetryOptionsDropdown = ({ tableId, tableName }: RetryOptionsDropdow ) }, }) - const { mutate: startPipeline, isPending: isRestartingPipeline } = useStartPipelineMutation({ - onSuccess: () => { - toast.success('Pipeline restarted successfully') - setIsOpen(false) - }, - onError: (error) => { - toast.error(`Failed to restart pipeline: ${error.message}`) - setIsOpen(false) - }, - }) const isLoading = isRollingBack || isRestartingPipeline diff --git a/apps/studio/components/interfaces/Database/ETL/RowMenu.tsx b/apps/studio/components/interfaces/Database/ETL/RowMenu.tsx index 9a02bb49c42..8456c1dcaf4 100644 --- a/apps/studio/components/interfaces/Database/ETL/RowMenu.tsx +++ b/apps/studio/components/interfaces/Database/ETL/RowMenu.tsx @@ -5,6 +5,7 @@ import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { ReplicationPipelineStatusData } from 'data/etl/pipeline-status-query' import { Pipeline } from 'data/etl/pipelines-query' +import { useRestartPipelineHelper } from 'data/etl/restart-pipeline-helper' import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation' import { useStopPipelineMutation } from 'data/etl/stop-pipeline-mutation' import { @@ -58,6 +59,7 @@ export const RowMenu = ({ const { mutateAsync: startPipeline } = useStartPipelineMutation() const { mutateAsync: stopPipeline } = useStopPipelineMutation() + const { restartPipeline } = useRestartPipelineHelper() const { getRequestStatus, setRequestStatus: setGlobalRequestStatus } = usePipelineRequestStatus() const requestStatus = pipeline?.id ? getRequestStatus(pipeline.id) @@ -117,7 +119,7 @@ export const RowMenu = ({ try { setGlobalRequestStatus(pipeline.id, PipelineStatusRequestStatus.RestartRequested, statusName) - await startPipeline({ projectRef, pipelineId: pipeline.id }) + await restartPipeline({ projectRef, pipelineId: pipeline.id }) } catch (error) { setGlobalRequestStatus(pipeline.id, PipelineStatusRequestStatus.None) toast.error(PIPELINE_ERROR_MESSAGES.ENABLE_DESTINATION) diff --git a/apps/studio/components/interfaces/Database/ETL/UpdateVersionModal.tsx b/apps/studio/components/interfaces/Database/ETL/UpdateVersionModal.tsx index 585241a7adb..78439c97b4b 100644 --- a/apps/studio/components/interfaces/Database/ETL/UpdateVersionModal.tsx +++ b/apps/studio/components/interfaces/Database/ETL/UpdateVersionModal.tsx @@ -4,12 +4,19 @@ import { useParams } from 'common' import { useReplicationPipelineStatusQuery } from 'data/etl/pipeline-status-query' import { useReplicationPipelineVersionQuery } from 'data/etl/pipeline-version-query' import { Pipeline } from 'data/etl/pipelines-query' -import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation' +import { useRestartPipelineHelper } from 'data/etl/restart-pipeline-helper' import { useUpdatePipelineVersionMutation } from 'data/etl/update-pipeline-version-mutation' +import { ChevronDown } from 'lucide-react' import { PipelineStatusRequestStatus, usePipelineRequestStatus, } from 'state/replication-pipeline-request-status' +import { + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + DialogSectionSeparator, +} from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { getStatusName } from './Pipeline.utils' import { PipelineStatusName, STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants' @@ -25,7 +32,7 @@ interface UpdateVersionModalProps { export const UpdateVersionModal = ({ visible, pipeline, - confirmLabel = 'Update and restart', + confirmLabel, confirmLabelLoading = 'Updating', onClose, }: UpdateVersionModalProps) => { @@ -38,6 +45,7 @@ export const UpdateVersionModal = ({ ) const pipelineStatus = pipelineStatusData?.status const statusName = getStatusName(pipelineStatus) + const isStopped = statusName === PipelineStatusName.STOPPED const { data: versionData } = useReplicationPipelineVersionQuery({ projectRef, @@ -47,7 +55,7 @@ export const UpdateVersionModal = ({ const newVersionName = versionData?.new_version?.name const { mutateAsync: updatePipelineVersion } = useUpdatePipelineVersionMutation() - const { mutateAsync: startPipeline } = useStartPipelineMutation() + const { restartPipeline } = useRestartPipelineHelper() const onConfirmUpdate = async () => { if (!projectRef || !pipeline?.id) return @@ -64,16 +72,13 @@ export const UpdateVersionModal = ({ return } - // Step 2: Reflect optimistic restart (only if currently active) and close any panels - const isActive = - statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED - - if (isActive) { + // Step 2: Reflect optimistic restart (only if not stopped) and close any panels + if (!isStopped) { setRequestStatus(pipeline.id, PipelineStatusRequestStatus.RestartRequested, statusName) - // Step 3: Restart the pipeline + // Step 3: Restart the pipeline (stop + start) try { - await startPipeline({ projectRef, pipelineId: pipeline.id }) + await restartPipeline({ projectRef, pipelineId: pipeline.id }) toast.success('Pipeline successfully updated and is currently restarting') } catch (e: any) { // Clear optimistic state and surface a single concise error @@ -89,34 +94,46 @@ export const UpdateVersionModal = ({ return ( -

- During the update process, the replication pauses and resumes. -

-

- If a long‑running transaction is in progress, some records may be reprocessed due to - PostgreSQL logical replication limitations. -

-
- ), - }} > -

- Pipeline will be updated from {currentVersionName ?? 'Current version'} to{' '} - {newVersionName ?? 'New version'}. -

-

Confirm to update pipeline? This action cannot be undone.

+
+

+ A new pipeline image is available with improvements and bug fixes. Proceed to update? +

+ {!isStopped && ( +

+ The pipeline will automatically restart when updating. Replication will continue from + where it left off. +

+ )} +
+ + + + +

View version update details

+ +
+ +
+
+

Current version:

{' '} + {currentVersionName ?? 'Unknown'} +
+
+

New version:

{' '} + {newVersionName ?? 'Unknown'} +
+
+
+
) } diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx index 31ce6352910..757995ea243 100644 --- a/apps/studio/components/interfaces/Sidebar.tsx +++ b/apps/studio/components/interfaces/Sidebar.tsx @@ -430,6 +430,8 @@ const OrganizationLinks = () => { }, ] + if (!organizationSlug) return null + return ( diff --git a/apps/studio/data/etl/restart-pipeline-helper.ts b/apps/studio/data/etl/restart-pipeline-helper.ts new file mode 100644 index 00000000000..75c5b022f29 --- /dev/null +++ b/apps/studio/data/etl/restart-pipeline-helper.ts @@ -0,0 +1,40 @@ +import { useStopPipelineMutation } from './stop-pipeline-mutation' +import { useStartPipelineMutation } from './start-pipeline-mutation' + +export interface RestartPipelineParams { + projectRef: string + pipelineId: number +} + +/** + * Helper hook that provides a restart function which properly stops and then starts a pipeline. + * + * ## Why Stop + Start? + * + * This explicit two-step restart process is necessary to work around edge cases where Kubernetes + * doesn't properly recreate pods when using the start endpoint alone on a running pipeline. This happens + * because crash looping pods are not restarted if the resource is patched. We will try to find a better + * solution for this in the future. + */ +export const useRestartPipelineHelper = () => { + const { mutateAsync: stopPipeline } = useStopPipelineMutation() + const { mutateAsync: startPipeline } = useStartPipelineMutation() + + const restartPipeline = async ({ projectRef, pipelineId }: RestartPipelineParams) => { + // Step 1: Stop the pipeline to ensure pods are fully terminated + try { + await stopPipeline({ projectRef, pipelineId }) + } catch (error: any) { + throw new Error(`Failed to stop pipeline: ${error.message}`) + } + + // Step 2: Start the pipeline to create fresh pods with clean state + try { + await startPipeline({ projectRef, pipelineId }) + } catch (error: any) { + throw new Error(`Failed to start pipeline: ${error.message}`) + } + } + + return { restartPipeline } +} diff --git a/apps/studio/data/etl/use-table-reset.ts b/apps/studio/data/etl/use-table-reset.ts new file mode 100644 index 00000000000..86136afdfda --- /dev/null +++ b/apps/studio/data/etl/use-table-reset.ts @@ -0,0 +1,70 @@ +import { useState } from 'react' +import { toast } from 'sonner' +import { RollbackType, useRollbackTableMutation } from './rollback-table-mutation' +import { useRestartPipelineHelper } from './restart-pipeline-helper' + +interface UseTableResetParams { + tableName: string + onSuccess?: () => void + onError?: (error: Error) => void +} + +/** + * Custom hook that encapsulates the logic for resetting a table and restarting the pipeline. + * Provides unified error handling and loading states for table reset operations. + */ +export const useTableReset = ({ tableName, onSuccess, onError }: UseTableResetParams) => { + const [isRestartingPipeline, setIsRestartingPipeline] = useState(false) + const { restartPipeline } = useRestartPipelineHelper() + + const { mutate: rollbackTable, isLoading: isRollingBack } = useRollbackTableMutation({ + onSuccess: async (_, vars) => { + const { projectRef, pipelineId } = vars + toast.success(`Table "${tableName}" reset successfully and pipeline is being restarted`) + + setIsRestartingPipeline(true) + try { + await restartPipeline({ projectRef, pipelineId }) + toast.success('Pipeline restarted successfully') + onSuccess?.() + } catch (error: any) { + const errorMessage = `Failed to restart pipeline: ${error.message}` + toast.error(errorMessage) + onError?.(new Error(errorMessage)) + } finally { + setIsRestartingPipeline(false) + } + }, + onError: (error) => { + const errorMessage = `Failed to reset table: ${error.message}` + toast.error(errorMessage) + onError?.(new Error(errorMessage)) + }, + }) + + const resetTable = ({ + projectRef, + pipelineId, + tableId, + rollbackType = 'full' as RollbackType, + }: { + projectRef: string + pipelineId: number + tableId: number + rollbackType?: RollbackType + }) => { + rollbackTable({ + projectRef, + pipelineId, + tableId, + rollbackType, + }) + } + + return { + resetTable, + isRollingBack, + isRestartingPipeline, + isResetting: isRollingBack || isRestartingPipeline, + } +} diff --git a/packages/ui-patterns/src/Dialogs/ConfirmationModal.tsx b/packages/ui-patterns/src/Dialogs/ConfirmationModal.tsx index 7db9f803e4f..bea5d1633a5 100644 --- a/packages/ui-patterns/src/Dialogs/ConfirmationModal.tsx +++ b/packages/ui-patterns/src/Dialogs/ConfirmationModal.tsx @@ -32,6 +32,7 @@ export interface ConfirmationModalProps { title?: string description?: string | React.ReactNode } + className?: string } export const ConfirmationModal = forwardRef< @@ -54,6 +55,7 @@ export const ConfirmationModal = forwardRef< children, variant = 'default', disabled, + className, ...props }, ref @@ -105,7 +107,9 @@ export const ConfirmationModal = forwardRef< )} {children && ( <> - {children} + + {children} + )}