mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
## Summary Adds initial-copy scoping to Pipelines in Studio. Users can copy all existing rows, skip all initial copies, copy only selected publication tables, or skip selected table copies. All publication tables continue streaming new changes regardless of the initial-copy policy. The policy now round-trips through create, edit, validation, and the generated Management API contract. Initial-copy estimates and table-restart confirmations use the same scope. Edit requests also preserve redacted credentials and pipeline settings that Studio does not own. This completes the Studio layer of the [ETL API change](https://github.com/supabase/etl/pull/897) and [Management API change](https://github.com/supabase/platform/pull/35479). ## Screenshots ### Selector <img width="1153" height="465" alt="image" src="https://github.com/user-attachments/assets/bf615e82-ee61-4222-979d-a8695a957e82" /> ### Select certain tables only <img width="1153" height="465" alt="image" src="https://github.com/user-attachments/assets/28adaa24-f239-4d1d-8fb8-fdb1988320cd" /> ### Confirm copy costs As the final step before the pipeline is created: <img width="597" height="619" alt="image" src="https://github.com/user-attachments/assets/a660bd87-bfb8-41c5-8099-4cdbdef943bf" /> ### Policy-aware initial-copy estimate #### Copy no table is selected <img width="407" height="464" alt="image" src="https://github.com/user-attachments/assets/99d859ec-2ec3-452a-ab69-11924a8db260" /> #### Some tables are selected <img width="407" height="464" alt="image" src="https://github.com/user-attachments/assets/e68aedf4-66bc-4372-98ef-0dd7fecef324" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added configurable “initial table copy” policies (copy/skip all and copy/skip selected) during replication setup, including table-picker behavior, pruning of stale selections, and updated restart/cost estimates. - **Bug Fixes** - Improved restart flows to consistently use `schema.table` identity and simplified “errored tables” targeting to match error-state tables. - Reduced unnecessary loading by gating publication/table fetches to when panels are visible; improved validation/toast handling when publication tables are unavailable. - **Tests** - Added/expanded coverage for destination form submission, table-copy selection, restart/cost dialogs, and copy-estimate summarization. - **Style** - Refreshed warning/label text for clearer configuration and confirmation messaging. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Victor Farazdagi <simple.square@gmail.com> Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
196 lines
6.5 KiB
TypeScript
196 lines
6.5 KiB
TypeScript
import { useParams } from 'common'
|
|
import { useMemo } from 'react'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from 'ui'
|
|
|
|
import { PipelineStatusName } from './Replication.constants'
|
|
import { RestartCostEstimate } from './RestartCostEstimate'
|
|
import { getTableCopyTargets, type TableSyncCopyConfig } from './TableSyncCopy.utils'
|
|
import { ReplicationPipelineTableStatus } from '@/data/replication/pipeline-replication-status-query'
|
|
import { useRollbackTablesMutation } from '@/data/replication/rollback-tables-mutation'
|
|
|
|
interface BatchRestartDialogProps {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
mode: 'all' | 'errored'
|
|
tables: ReplicationPipelineTableStatus[]
|
|
sourceId?: number
|
|
publicationName?: string
|
|
tableSyncCopy?: TableSyncCopyConfig
|
|
pipelineStatusName?: PipelineStatusName
|
|
onRestartStart?: (tableIds: number[]) => void
|
|
onRestartComplete?: (tableIds: number[]) => void
|
|
}
|
|
|
|
export const BatchRestartDialog = ({
|
|
open,
|
|
onOpenChange,
|
|
mode,
|
|
tables,
|
|
sourceId,
|
|
publicationName,
|
|
tableSyncCopy,
|
|
pipelineStatusName,
|
|
onRestartStart,
|
|
onRestartComplete,
|
|
}: BatchRestartDialogProps) => {
|
|
const { ref: projectRef, pipelineId: _pipelineId } = useParams()
|
|
const pipelineId = Number(_pipelineId)
|
|
const affectedTables = useMemo(() => {
|
|
if (mode === 'all') {
|
|
return tables
|
|
} else {
|
|
return tables.filter((table) => table.state.name === 'error')
|
|
}
|
|
}, [mode, tables])
|
|
const affectedTableIds = useMemo(() => affectedTables.map((table) => table.id), [affectedTables])
|
|
|
|
const copiedTables = useMemo(
|
|
() => getTableCopyTargets(affectedTables, tableSyncCopy),
|
|
[affectedTables, tableSyncCopy]
|
|
)
|
|
|
|
const initialCopyDescription =
|
|
copiedTables.length === 0 ? (
|
|
<li>
|
|
<strong>No table will run an initial copy.</strong> New changes will resume streaming
|
|
without backfilling existing source rows. There is no additional initial-copy charge.
|
|
</li>
|
|
) : copiedTables.length === affectedTables.length ? (
|
|
<li>
|
|
<strong>
|
|
{copiedTables.length === 1 ? 'The table' : `All ${copiedTables.length} tables`} will run
|
|
an initial copy.
|
|
</strong>{' '}
|
|
Existing rows will be copied again from the source and billed in addition to previous
|
|
initial copies.
|
|
</li>
|
|
) : (
|
|
<li>
|
|
<strong>
|
|
{copiedTables.length} of {affectedTables.length} tables will run an initial copy.
|
|
</strong>{' '}
|
|
The remaining tables will resume streaming without a backfill. Copied rows are billed in
|
|
addition to previous initial copies.
|
|
</li>
|
|
)
|
|
|
|
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.`
|
|
)
|
|
},
|
|
onSettled: () => {
|
|
onRestartComplete?.(affectedTableIds)
|
|
onOpenChange(false)
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to restart replication: ${error.message}`)
|
|
},
|
|
})
|
|
|
|
const handleReset = async () => {
|
|
if (!projectRef) return toast.error('Project ref is required')
|
|
|
|
onRestartStart?.(affectedTableIds)
|
|
|
|
try {
|
|
await rollbackTables({
|
|
projectRef,
|
|
pipelineId,
|
|
target: mode === 'all' ? { type: 'all_tables' } : { type: 'all_errored_tables' },
|
|
rollbackType: 'full',
|
|
pipelineStatusName,
|
|
})
|
|
} catch (error) {}
|
|
}
|
|
|
|
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">
|
|
{initialCopyDescription}
|
|
<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: '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">
|
|
{initialCopyDescription}
|
|
<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',
|
|
}
|
|
|
|
return (
|
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{dialogContent.title}</AlertDialogTitle>
|
|
<AlertDialogDescription asChild>{dialogContent.description}</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<RestartCostEstimate
|
|
open={open}
|
|
projectRef={projectRef}
|
|
sourceId={sourceId}
|
|
publicationName={publicationName}
|
|
tables={copiedTables}
|
|
/>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel disabled={isResetting}>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction disabled={isResetting} onClick={handleReset} variant="warning">
|
|
{isResetting ? 'Restarting replication...' : dialogContent.action}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
)
|
|
}
|