Hello
}\n\n```vue\n{{ name }}
\n```', - }, - { - name: 'HTML embedded tagged templates', - lang: 'html', - code: '', - }, -] - -describe('selective code block highlighting', () => { - let baseline: Awaited+ {recoveryCodesStatus.data.remaining}/{recoveryCodesStatus.data.total} recovery + codes available +
+- This will restart replication for all {affectedTables.length} table - {affectedTables.length === 1 ? '' : 's'} in this pipeline from scratch: -
-- This will restart replication for all{' '} - {affectedTables.length} currently failed tables from scratch: -
-- {isValidating - ? 'Validating destination configuration...' - : editMode - ? existingDestination?.enabled - ? 'Updating destination and restarting pipeline...' - : 'Updating destination and starting pipeline...' - : 'Creating pipeline...'} -
+{getSavingMessage()}
) : ( diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/useDestinationForm.test.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/useDestinationForm.test.tsx index 32771ae9e24..f161df2b25a 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/useDestinationForm.test.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/useDestinationForm.test.tsx @@ -1,181 +1,244 @@ -import { act, renderHook } from '@testing-library/react' +import { QueryClient } from '@tanstack/react-query' +import { act, waitFor } from '@testing-library/react' +import type { components } from 'api-types' +import { HttpResponse } from 'msw' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { DestinationPanelSchemaType } from './DestinationForm.schema' import { useDestinationForm } from './useDestinationForm' +import { replicationKeys } from '@/data/replication/keys' +import { + PipelineRequestStatusProvider, + PipelineStatusRequestStatus, + usePipelineRequestStatus, +} from '@/state/replication-pipeline-request-status' +import { customRenderHook, CustomWrapper } from '@/tests/lib/custom-render' +import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw' -const mocks = vi.hoisted(() => ({ - validateDestination: vi.fn(), - validatePipeline: vi.fn(), - createS3AccessKey: vi.fn(), - createNamespace: vi.fn(), - createDestinationPipeline: vi.fn(), - updateDestinationPipeline: vi.fn(), - startPipeline: vi.fn(), - setRequestStatus: vi.fn(), -})) +type ValidationResponse = components['schemas']['ValidatePipelineResponse_Output'] +const updateRequests: unknown[] = [] +const validationRequests: unknown[] = [] +const startRequests = vi.fn() +const createRequests = vi.fn() +let validationResponse: ValidationResponse -vi.mock('common', () => ({ useParams: () => ({ ref: 'project-ref' }) })) -vi.mock('@/data/replication/sources-query', () => ({ - useReplicationSourcesQuery: () => ({ - data: { sources: [{ id: 42, name: 'project-ref' }] }, - }), -})) -vi.mock('@/data/replication/validate-destination-mutation', () => ({ - useValidateDestinationMutation: () => ({ - mutateAsync: mocks.validateDestination, - isPending: false, - }), -})) -vi.mock('@/data/replication/validate-pipeline-mutation', () => ({ - useValidatePipelineMutation: () => ({ - mutateAsync: mocks.validatePipeline, - isPending: false, - }), -})) -vi.mock('@/data/storage/s3-access-key-create-mutation', () => ({ - useS3AccessKeyCreateMutation: () => ({ - mutateAsync: mocks.createS3AccessKey, - isPending: false, - }), -})) -vi.mock('@/data/storage/iceberg-namespace-create-mutation', () => ({ - useIcebergNamespaceCreateMutation: () => ({ - mutateAsync: mocks.createNamespace, - isPending: false, - }), -})) -vi.mock('@/data/replication/create-destination-pipeline-mutation', () => ({ - useCreateDestinationPipelineMutation: () => ({ - mutateAsync: mocks.createDestinationPipeline, - isPending: false, - }), -})) -vi.mock('@/data/replication/update-destination-pipeline-mutation', () => ({ - useUpdateDestinationPipelineMutation: () => ({ - mutateAsync: mocks.updateDestinationPipeline, - isPending: false, - }), -})) -vi.mock('@/data/replication/start-pipeline-mutation', () => ({ - useStartPipelineMutation: () => ({ - mutateAsync: mocks.startPipeline, - isPending: false, - }), -})) -vi.mock('@/state/replication-pipeline-request-status', () => ({ - PipelineStatusRequestStatus: { - RestartRequested: 'restart-requested', - StartRequested: 'start-requested', - }, - usePipelineRequestStatus: () => ({ setRequestStatus: mocks.setRequestStatus }), -})) - -const formData = { +const formData: DestinationPanelSchemaType = { name: 'Analytics', publicationName: 'analytics', tableSyncCopyMode: 'include_tables', tableSyncCopyTableIds: ['101'], maxFillMs: 500, + maxTableSyncWorkers: 4, + maxCopyConnectionsPerTable: 1, + maxStalenessMins: 0, projectId: 'example-project', datasetId: 'analytics', serviceAccountKey: '', connectionPoolSize: 5, -} as DestinationPanelSchemaType +} -describe('useDestinationForm validation', () => { +const renderDestinationForm = async () => { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + const view = customRenderHook( + () => ({ + ...useDestinationForm({ selectedType: 'BigQuery' }), + requestStatus: usePipelineRequestStatus().getRequestStatus(8), + }), + { + wrapper: ({ children }) => ( +Slot statuses
-- How safely your database is keeping the changes the pipeline still needs. -
-- {table.schema}.{table.name} -
+- Replication is being restarted for this table. The pipeline will restart automatically. + Resetting. The pipeline will {pipelineAction} automatically…
) : showDisabledState ? ({disabledStateMessage}
- ) : ( -{statusConfig.description}.
+{detailsLine}
)} -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.
- 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..a9972791322 100644 --- a/apps/studio/components/interfaces/Database/Replication/RestartTableDialog.tsx +++ b/apps/studio/components/interfaces/Database/Replication/RestartTableDialog.tsx @@ -11,22 +11,27 @@ import { AlertDialogTitle, } from 'ui' -import { PipelineStatusName } from './Replication.constants' +import { getRestartRequestStatus } from './Pipeline.utils' +import type { PipelineStatusName } from './Replication.constants' import { RestartCostEstimate } from './RestartCostEstimate' import { shouldCopyTable, type ReplicationTableIdentity } from './TableSyncCopy.utils' import { useRollbackTablesMutation } from '@/data/replication/rollback-tables-mutation' import type { TableSyncCopyConfig } from '@/data/replication/types' +import { + PipelineStatusRequestStatus, + usePipelineRequestStatus, +} from '@/state/replication-pipeline-request-status' interface RestartTableDialogProps { + pipelineStatusName?: PipelineStatusName open: boolean onOpenChange: (open: boolean) => void table: ReplicationTableIdentity tableSyncCopy?: TableSyncCopyConfig | null sourceId?: number publicationName?: string - pipelineStatusName?: PipelineStatusName - onRestartStart?: () => void - onRestartComplete?: () => void + onResetStart?: (tableId: number) => void + onResetComplete?: (tableId: number) => void } export const RestartTableDialog = ({ @@ -37,84 +42,57 @@ export const RestartTableDialog = ({ sourceId, publicationName, pipelineStatusName, - onRestartStart, - onRestartComplete, + onResetStart, + onResetComplete, }: RestartTableDialogProps) => { const { ref: projectRef, pipelineId: _pipelineId } = useParams() const pipelineId = Number(_pipelineId) + const { runWithRequestStatus } = usePipelineRequestStatus() + const restartRequestStatus = getRestartRequestStatus(pipelineStatusName) const tableName = `${table.schema}.${table.name}` const willCopyTable = shouldCopyTable(tableSyncCopy, table.id) - - const { mutate: rollbackTables, isPending: isResetting } = useRollbackTablesMutation({ + const { mutateAsync: rollbackTables, isPending: isResetting } = useRollbackTablesMutation({ onSuccess: () => { - toast.success( - `Restarting replication for "${tableName}". Pipeline will ${pipelineStatusName === PipelineStatusName.STOPPED ? 'start' : 'restart'} automatically.` - ) - }, - onSettled: () => { - onRestartComplete?.() + toast.success(`Resetting "${tableName}"`) onOpenChange(false) }, onError: (error) => { - toast.error(`Failed to restart replication: ${error.message}`) + toast.error(`Failed to reset table: ${error.message}`) }, }) - const handleReset = () => { + const handleReset = async () => { if (!projectRef) return toast.error('Project ref is required') if (!pipelineId) return toast.error('Pipeline ID is required') + onResetStart?.(table.id) - onRestartStart?.() - rollbackTables({ - projectRef, - pipelineId, - target: { type: 'single_table', table_id: table.id }, - rollbackType: 'full', - pipelineStatusName, - }) + try { + await runWithRequestStatus(pipelineId, restartRequestStatus, () => + rollbackTables({ + projectRef, + pipelineId, + target: { type: 'single_table', table_id: table.id }, + }) + ) + } finally { + onResetComplete?.(table.id) + } } + const resetDescription = willCopyTable + ? 'This resets the table, deletes its destination data, and syncs existing rows again.' + : 'This resets the table and deletes its destination data. Initial sync is skipped, so replication resumes with new changes only.' + const shouldRestartPipeline = restartRequestStatus !== PipelineStatusRequestStatus.None + const consequence = shouldRestartPipeline + ? `${resetDescription} The pipeline restarts automatically to apply the reset.` + : resetDescription + return ({tableName}
-
- This will restart replication for{' '}
- {tableName} from scratch:
-
Update available
Edit pipeline
Delete pipeline
- {isStopped - ? 'A newer pipeline version is available with improvements and bug fixes.' - : 'A newer pipeline version is available with improvements and bug fixes. The pipeline will restart and continue from where it left off.'} + {shouldRestart + ? 'A newer pipeline version is available with improvements and bug fixes. The pipeline will restart and continue from where it left off.' + : 'A newer pipeline version is available with improvements and bug fixes.'}