From 0b002892d78caa182d5825098294b2cf6401e01d Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:13:51 +1000 Subject: [PATCH] refactor(studio): simplify pipeline reset dialogs (#50443) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Studio UI refactor. ## What is the current behavior? Pipeline table reset dialogs repeat explanatory content and use more layout than the reset decision needs. ## What is the new behavior? Simplifies the single-table and batch reset confirmations while preserving their cost estimate, destructive consequences, and existing reset mutations. | Before | After | | --- | --- | | CleanShot 2026-09-16 at 12 54
36@2x | CleanShot 2026-09-16 at 17 01
11@2x | This is an independent slice extracted from #49630. The related review series is this one, #50444, #50445, #50446, then #49630. ## To test 1. Open `/project//database/replication` and select a pipeline. 2. Reset one replicated table and confirm the dialog explains that destination data will be deleted and resynchronised. 3. Choose **Reset all tables** and confirm the batch dialog shows the same concise treatment. ## Summary by CodeRabbit ## UI Updates * **UI Updates** * Renamed replication “restart” actions to “reset” across dialogs, buttons, notifications, and cost estimates. * Updated messaging to clarify whether the pipeline will start or restart automatically after resetting. * Added clearer initial-sync guidance for all, some, or none of the affected tables. * Improved reset cost estimate messaging, including when no additional initial-sync charge applies. * Updated reset dialogs with clearer titles, descriptions, loading states, and error messages. * Disabled reset actions when pipeline status is unavailable. --- .../Replication/BatchRestartDialog.test.tsx | 24 +++- .../Replication/BatchRestartDialog.tsx | 106 +++++------------- .../Replication/RestartCostEstimate.tsx | 12 +- .../Replication/RestartTableDialog.tsx | 52 ++------- 4 files changed, 67 insertions(+), 127 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.test.tsx b/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.test.tsx index cae640ff9a7..19b7ccbf2a0 100644 --- a/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.test.tsx +++ b/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.test.tsx @@ -2,6 +2,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' import { BatchRestartDialog } from './BatchRestartDialog' +import { PipelineStatusName } from './Replication.constants' import type { ReplicationPipelineTableStatus } from '@/data/replication/pipeline-replication-status-query' const mocks = vi.hoisted(() => ({ @@ -58,15 +59,21 @@ describe('BatchRestartDialog', () => { mode="errored" tables={tables} tableSyncCopy={{ type: 'include_tables', table_ids: [1, 2] }} + pipelineStatusName={PipelineStatusName.STARTED} onRestartStart={onRestartStart} /> ) - expect(screen.getByText(/3 currently failed tables/)).toBeInTheDocument() + expect(screen.getByText(/This resets 3 failed tables/)).toBeInTheDocument() + expect( + screen.getByText( + /2 of 3 tables will sync existing rows again. The remaining 1 table will skip initial sync/ + ) + ).toBeInTheDocument() expect(screen.getByTestId('copy-targets')).toHaveTextContent('public.table_1,public.table_2') await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'Restart failed tables' })) + fireEvent.click(screen.getByRole('button', { name: 'Reset failed tables' })) }) expect(onRestartStart).toHaveBeenCalledWith([1, 2, 3]) @@ -78,4 +85,17 @@ describe('BatchRestartDialog', () => { }) ) }) + + it('prevents a reset when the pipeline status is unavailable', () => { + render( + + ) + + expect(screen.getByRole('button', { name: 'Reset all tables' })).toBeDisabled() + }) }) diff --git a/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.tsx b/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.tsx index 765b929e4c7..ad806e1f167 100644 --- a/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.tsx +++ b/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.tsx @@ -54,43 +54,19 @@ export const BatchRestartDialog = ({ } }, [mode, tables]) const affectedTableIds = useMemo(() => affectedTables.map((table) => table.id), [affectedTables]) + const isPipelineStatusUnavailable = pipelineStatusName === undefined const copiedTables = useMemo( () => getTableCopyTargets(affectedTables, tableSyncCopy), [affectedTables, tableSyncCopy] ) - - const initialSyncDescription = - copiedTables.length === 0 ? ( -
  • - No table will run an initial sync. Replication will resume with new changes - only, without syncing existing source rows. There is no additional initial sync charge. -
  • - ) : copiedTables.length === affectedTables.length ? ( -
  • - - {copiedTables.length === 1 - ? 'The table will run its initial sync again.' - : `All ${copiedTables.length} tables will run initial sync again.`} - {' '} - Existing source rows will be synced again. Data successfully processed during this initial - sync is billed again. -
  • - ) : ( -
  • - - {copiedTables.length} of {affectedTables.length} tables will run initial sync again. - {' '} - Existing source rows for those tables will be synced again and billed again. The remaining - tables will resume replication with new changes only. -
  • - ) + const pipelineAction = pipelineStatusName === PipelineStatusName.STOPPED ? 'start' : 'restart' const { mutateAsync: rollbackTables, isPending: isResetting } = useRollbackTablesMutation({ onSuccess: (data) => { const count = data.tables.length toast.success( - `Restarting replication for ${count} table${count > 1 ? 's' : ''}. Pipeline will restart automatically.` + `Resetting ${count} table${count > 1 ? 's' : ''}. Pipeline will ${pipelineAction} automatically.` ) }, onSettled: () => { @@ -98,12 +74,13 @@ export const BatchRestartDialog = ({ onOpenChange(false) }, onError: (error) => { - toast.error(`Failed to restart replication: ${error.message}`) + toast.error(`Failed to reset tables: ${error.message}`) }, }) const handleReset = async () => { if (!projectRef) return toast.error('Project ref is required') + if (isPipelineStatusUnavailable) return onRestartStart?.(affectedTableIds) @@ -118,57 +95,28 @@ export const BatchRestartDialog = ({ } catch (error) {} } + const count = affectedTables.length + const tableWord = count === 1 ? 'table' : 'tables' + const remainingTableCount = count - copiedTables.length + const remainingTableWord = remainingTableCount === 1 ? 'table' : 'tables' + const initialSyncDescription = + copiedTables.length === 0 + ? 'Initial sync is skipped, so replication resumes with new changes only.' + : copiedTables.length === affectedTables.length + ? 'Existing rows will sync again.' + : `${copiedTables.length} of ${count} ${tableWord} will sync existing rows again. The remaining ${remainingTableCount} ${remainingTableWord} will skip initial sync and resume with new changes only.` + const dialogContent = mode === 'all' ? { - title: 'Restart all tables', - description: ( -
    -

    - This will restart replication for all {affectedTables.length} table - {affectedTables.length === 1 ? '' : 's'} in this pipeline from scratch: -

    -
      - {initialSyncDescription} -
    • - All downstream data will be deleted. All replicated data will be - removed. -
    • -
    • - The pipeline will restart automatically. This is required to - apply this change. -
    • -
    -
    - ), - action: 'Restart all tables', + title: 'Reset all tables', + description: `This resets all ${count} ${tableWord}. Destination data will be deleted. ${initialSyncDescription} The pipeline will ${pipelineAction} automatically.`, + action: 'Reset all tables', } : { - title: 'Restart failed tables', - description: ( -
    -

    - This will restart replication for all{' '} - {affectedTables.length} currently failed tables from scratch: -

    -
      - {initialSyncDescription} -
    • - Existing downstream data will be deleted. Replicated data for - these tables will be removed. -
    • -
    • - Tables that are not failed remain untouched. The request resets - every table that is failed when it runs. -
    • -
    • - The pipeline will restart automatically. This is required to - apply this change. -
    • -
    -
    - ), - action: 'Restart failed tables', + title: 'Reset failed tables', + description: `This resets ${count} failed ${tableWord}. Destination data for those tables will be deleted. ${initialSyncDescription} The pipeline will ${pipelineAction} automatically. Other tables stay as they are.`, + action: 'Reset failed tables', } return ( @@ -176,7 +124,7 @@ export const BatchRestartDialog = ({ {dialogContent.title} - {dialogContent.description} + {dialogContent.description} Cancel - - {isResetting ? 'Restarting replication...' : dialogContent.action} + + {isResetting ? 'Resetting…' : dialogContent.action} diff --git a/apps/studio/components/interfaces/Database/Replication/RestartCostEstimate.tsx b/apps/studio/components/interfaces/Database/Replication/RestartCostEstimate.tsx index 55bae36ad55..131b47a9162 100644 --- a/apps/studio/components/interfaces/Database/Replication/RestartCostEstimate.tsx +++ b/apps/studio/components/interfaces/Database/Replication/RestartCostEstimate.tsx @@ -41,13 +41,13 @@ export const RestartCostEstimate = ({ [estimate, tables] ) return ( -
    +
    {tables.length === 0 ? (

    No additional initial sync charge

    - This restart will skip initial sync based on the pipeline's settings. + This reset will skip initial sync based on the pipeline’s settings.

    @@ -71,18 +71,18 @@ export const RestartCostEstimate = ({
    ) : ( -

    - A cost estimate is unavailable. You can still restart the{' '} +

    + A cost estimate is unavailable. You can still reset the{' '} {tables.length === 1 ? 'table' : 'tables'}.

    )} {restartEstimate?.isComplete && restartEstimate.hasRowFilteredTables && ( -

    +

    *Row filters can reduce the data processed compared with this estimate.

    )} {restartEstimate?.isComplete && ( -

    +

    Quick planning estimate; the final charge is based on successfully processed initial sync data, which is billed again.

    diff --git a/apps/studio/components/interfaces/Database/Replication/RestartTableDialog.tsx b/apps/studio/components/interfaces/Database/Replication/RestartTableDialog.tsx index c9cb1f8b4c5..01441eb6ca9 100644 --- a/apps/studio/components/interfaces/Database/Replication/RestartTableDialog.tsx +++ b/apps/studio/components/interfaces/Database/Replication/RestartTableDialog.tsx @@ -44,19 +44,18 @@ export const RestartTableDialog = ({ const pipelineId = Number(_pipelineId) const tableName = `${table.schema}.${table.name}` const willCopyTable = shouldCopyTable(tableSyncCopy, table.id) + const pipelineAction = pipelineStatusName === PipelineStatusName.STOPPED ? 'start' : 'restart' const { mutate: rollbackTables, isPending: isResetting } = useRollbackTablesMutation({ onSuccess: () => { - toast.success( - `Restarting replication for "${tableName}". Pipeline will ${pipelineStatusName === PipelineStatusName.STOPPED ? 'start' : 'restart'} automatically.` - ) + toast.success(`Resetting "${tableName}". Pipeline will ${pipelineAction} automatically.`) }, onSettled: () => { onRestartComplete?.() onOpenChange(false) }, onError: (error) => { - toast.error(`Failed to restart replication: ${error.message}`) + toast.error(`Failed to reset table: ${error.message}`) }, }) @@ -74,47 +73,16 @@ export const RestartTableDialog = ({ }) } + const consequence = willCopyTable + ? `Destination data for this table will be deleted, existing rows will sync again, and the pipeline will ${pipelineAction} automatically.` + : `Destination data for this table will be deleted. Initial sync is skipped for this table, so replication resumes with new changes only. The pipeline will ${pipelineAction} automatically.` + return ( - - Restart replication for {tableName} - - -
    -

    - This will restart replication for{' '} - {tableName} from scratch: -

    -
      - {willCopyTable ? ( -
    • - The table's initial sync will restart. Existing source rows - will be synced again. Data successfully processed during this initial sync is - billed again. -
    • - ) : ( -
    • - The table will skip initial sync. Replication will resume with - new changes only, without syncing existing source rows. There is no additional - initial sync charge. -
    • - )} -
    • - Existing downstream data will be deleted. Any replicated data for - this table will be removed. -
    • -
    • - All other tables remain untouched. Only this table is affected. -
    • -
    • - The pipeline will restart automatically. This is required to - apply this change. -
    • -
    -
    -
    + Reset {tableName} + {consequence}
    Cancel - {isResetting ? 'Restarting replication...' : 'Restart replication'} + {isResetting ? 'Resetting…' : 'Reset table'}