mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
refactor(studio): simplify pipeline reset dialogs (#50443)
## 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 | | --- | --- | | <img width="854" height="1090" alt="CleanShot 2026-09-16 at 12 54 36@2x" src="https://github.com/user-attachments/assets/f9eef09b-89d1-4747-bc4c-e81fb64c584b" /> | <img width="840" height="742" alt="CleanShot 2026-09-16 at 17 01 11@2x" src="https://github.com/user-attachments/assets/fa45728c-ec66-45c8-9fef-9d2eb8310d4d" /> | 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/<ref>/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. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
cc540ff302
commit
0b002892d7
4 files changed
+67
-127
No files matched your search
@@ -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(
|
||||
<BatchRestartDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
mode="all"
|
||||
tables={[table(1, { name: 'following_wal' })]}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Reset all tables' })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@@ -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 ? (
|
||||
<li>
|
||||
<strong>No table will run an initial sync.</strong> Replication will resume with new changes
|
||||
only, without syncing existing source rows. There is no additional initial sync charge.
|
||||
</li>
|
||||
) : copiedTables.length === affectedTables.length ? (
|
||||
<li>
|
||||
<strong>
|
||||
{copiedTables.length === 1
|
||||
? 'The table will run its initial sync again.'
|
||||
: `All ${copiedTables.length} tables will run initial sync again.`}
|
||||
</strong>{' '}
|
||||
Existing source rows will be synced again. Data successfully processed during this initial
|
||||
sync is billed again.
|
||||
</li>
|
||||
) : (
|
||||
<li>
|
||||
<strong>
|
||||
{copiedTables.length} of {affectedTables.length} tables will run initial sync again.
|
||||
</strong>{' '}
|
||||
Existing source rows for those tables will be synced again and billed again. The remaining
|
||||
tables will resume replication with new changes only.
|
||||
</li>
|
||||
)
|
||||
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: (
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>
|
||||
This will restart replication for all {affectedTables.length} table
|
||||
{affectedTables.length === 1 ? '' : 's'} in this pipeline from scratch:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1.5 pl-2">
|
||||
{initialSyncDescription}
|
||||
<li>
|
||||
<strong>All downstream data will be deleted.</strong> All replicated data will be
|
||||
removed.
|
||||
</li>
|
||||
<li>
|
||||
<strong>The pipeline will restart automatically.</strong> This is required to
|
||||
apply this change.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
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: (
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>
|
||||
This will restart replication for all{' '}
|
||||
<strong>{affectedTables.length} currently failed tables</strong> from scratch:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1.5 pl-2">
|
||||
{initialSyncDescription}
|
||||
<li>
|
||||
<strong>Existing downstream data will be deleted.</strong> Replicated data for
|
||||
these tables will be removed.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tables that are not failed remain untouched.</strong> The request resets
|
||||
every table that is failed when it runs.
|
||||
</li>
|
||||
<li>
|
||||
<strong>The pipeline will restart automatically.</strong> This is required to
|
||||
apply this change.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
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 = ({
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{dialogContent.title}</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>{dialogContent.description}</AlertDialogDescription>
|
||||
<AlertDialogDescription>{dialogContent.description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<RestartCostEstimate
|
||||
open={open}
|
||||
@@ -187,8 +135,12 @@ export const BatchRestartDialog = ({
|
||||
/>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isResetting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction disabled={isResetting} onClick={handleReset} variant="warning">
|
||||
{isResetting ? 'Restarting replication...' : dialogContent.action}
|
||||
<AlertDialogAction
|
||||
disabled={isResetting || isPipelineStatusUnavailable}
|
||||
onClick={handleReset}
|
||||
variant="warning"
|
||||
>
|
||||
{isResetting ? 'Resetting…' : dialogContent.action}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -41,13 +41,13 @@ export const RestartCostEstimate = ({
|
||||
[estimate, tables]
|
||||
)
|
||||
return (
|
||||
<div className="border-t p-4">
|
||||
<div className="space-y-2 border-t px-5 py-4">
|
||||
{tables.length === 0 ? (
|
||||
<div className="flex items-center justify-between gap-x-6">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">No additional initial sync charge</p>
|
||||
<p className="text-xs text-foreground-lighter">
|
||||
This restart will skip initial sync based on the pipeline's settings.
|
||||
This reset will skip initial sync based on the pipeline’s settings.
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 font-mono text-lg font-semibold" translate="no">
|
||||
@@ -71,18 +71,18 @@ export const RestartCostEstimate = ({
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-foreground-lighter">
|
||||
A cost estimate is unavailable. You can still restart the{' '}
|
||||
<p className="text-sm text-foreground-light">
|
||||
A cost estimate is unavailable. You can still reset the{' '}
|
||||
{tables.length === 1 ? 'table' : 'tables'}.
|
||||
</p>
|
||||
)}
|
||||
{restartEstimate?.isComplete && restartEstimate.hasRowFilteredTables && (
|
||||
<p className="mt-2 text-xs text-foreground-lighter">
|
||||
<p className="text-xs text-foreground-lighter">
|
||||
*Row filters can reduce the data processed compared with this estimate.
|
||||
</p>
|
||||
)}
|
||||
{restartEstimate?.isComplete && (
|
||||
<p className="mt-2 text-xs text-foreground-lighter">
|
||||
<p className="text-xs text-foreground-lighter">
|
||||
Quick planning estimate; the final charge is based on successfully processed initial sync
|
||||
data, which is billed again.
|
||||
</p>
|
||||
|
||||
@@ -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 (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Restart replication for <code className="text-code-inline">{tableName}</code>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>
|
||||
This will restart replication for{' '}
|
||||
<code className="text-code-inline">{tableName}</code> from scratch:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1.5 pl-2">
|
||||
{willCopyTable ? (
|
||||
<li>
|
||||
<strong>The table's initial sync will restart.</strong> Existing source rows
|
||||
will be synced again. Data successfully processed during this initial sync is
|
||||
billed again.
|
||||
</li>
|
||||
) : (
|
||||
<li>
|
||||
<strong>The table will skip initial sync.</strong> Replication will resume with
|
||||
new changes only, without syncing existing source rows. There is no additional
|
||||
initial sync charge.
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<strong>Existing downstream data will be deleted.</strong> Any replicated data for
|
||||
this table will be removed.
|
||||
</li>
|
||||
<li>
|
||||
<strong>All other tables remain untouched.</strong> Only this table is affected.
|
||||
</li>
|
||||
<li>
|
||||
<strong>The pipeline will restart automatically.</strong> This is required to
|
||||
apply this change.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogTitle>Reset {tableName}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{consequence}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<RestartCostEstimate
|
||||
open={open}
|
||||
@@ -126,7 +94,7 @@ export const RestartTableDialog = ({
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isResetting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction disabled={isResetting} onClick={handleReset} variant="warning">
|
||||
{isResetting ? 'Restarting replication...' : 'Restart replication'}
|
||||
{isResetting ? 'Resetting…' : 'Reset table'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
Reference in new issue
Block a user