From 7fa436050a00a1b7c78b4bc87ee7eb3b483a78be Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:28:21 +1100 Subject: [PATCH] refactor(studio): compose the pipeline data section for wizard reuse (#51572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The pipeline wizard in #49243 needs publication, initial sync and replication settings together on its data step. ## Solution Compose `PipelineDataSection` from the existing controls and use it in preview-enabled creation. It includes the publication selector, initial-sync controls and data advanced settings, including BigQuery table layout. This preserves the grouping from #51570. Preview off and editing keep the existing layout. Validation and creation behaviour stay the same; no wizard routing is added. Stacked on #51570. ## Review instructions 1. Open the [Pipelines preview](https://studio-staging-git-dnywh-featpipeline-data-section-supabase.vercel.app/dashboard/project/_/database/pipelines), select a test project with Pipelines enabled and go to **Database → Pipelines**. Enable **Pipeline creation wizard** in Feature Previews, then click **Add pipeline**. Confirm **Data** follows **Destination connection**, with publication and initial sync above its **Advanced settings**. 2. Select a publication and change the initial-sync option. For selected-table options, confirm the publication tables are available. Click **New publication** in the publication selector and confirm the existing creation panel opens, then cancel it. 3. Expand **Advanced settings** under **Data**. Confirm replication settings appear, plus table layout for BigQuery, without connection pool size, staleness or role. Change **Batch wait time**, collapse and reopen the accordion, and confirm the value remains. Check desktop and phone widths. Cancel and discard changes; no credentials or pipeline creation are needed. 4. Turn the preview off and reopen **Add pipeline**. Confirm the original layout remains. With the preview on, edit an existing pipeline and confirm its original layout also remains. --- .../PipelineDataSection.test.tsx | 119 ++++++++++++++++++ .../CreatePipeline/PipelineDataSection.tsx | 28 +++++ .../DestinationForm/index.test.tsx | 1 + .../DestinationForm/index.tsx | 71 +++++------ 4 files changed, 182 insertions(+), 37 deletions(-) create mode 100644 apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.test.tsx create mode 100644 apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.tsx diff --git a/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.test.tsx b/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.test.tsx new file mode 100644 index 00000000000..90e69d6aebf --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.test.tsx @@ -0,0 +1,119 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import type { components } from 'api-types' +import { mockAnimationsApi } from 'jsdom-testing-mocks' +import { HttpResponse } from 'msw' +import { useForm } from 'react-hook-form' +import { Form } from 'ui' +import { describe, expect, it, vi } from 'vitest' + +import type { DestinationPanelSchemaType } from '../DestinationPanel/DestinationForm/DestinationForm.schema' +import type { DestinationType } from '../DestinationPanel/DestinationPanel.types' +import { PipelineDataSection } from './PipelineDataSection' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +mockAnimationsApi() +type ReplicationSourcesResponse = components['schemas']['SourcesResponse_Output'] +type PublicationDetailsResponse = components['schemas']['PublicationDetailsResponse_Output'] +type PublicationNamesResponse = components['schemas']['ReadPublicationsResponse_Output'] + +const mockSources: ReplicationSourcesResponse = { + sources: [ + { + id: 1, + name: 'default', + tenant_id: 'tenant', + config: { host: 'db.internal', name: 'main-db', port: 5432, username: 'etl_user' }, + }, + ], +} + +const mockPublicationDetails = (publishViaPartitionRoot: boolean): PublicationDetailsResponse => ({ + name: 'analytics', + config: { + type: 'tables', + tables: [], + operations: ['insert', 'update', 'delete', 'truncate'], + publish_via_partition_root: publishViaPartitionRoot, + }, + tables: [], +}) + +const mockPublicationRequests = (publishViaPartitionRoot: boolean) => { + addAPIMock({ + method: 'get', + path: '/platform/replication/:ref/sources', + response: () => HttpResponse.json(mockSources), + }) + addAPIMock({ + method: 'get', + path: '/platform/replication/v2/:ref/sources/:source_id/publications', + response: () => + HttpResponse.json({ + publications: [{ name: 'analytics' }], + }), + }) + addAPIMock({ + method: 'get', + path: '/platform/replication/v2/:ref/sources/:source_id/publications/:publication_name', + response: () => + HttpResponse.json( + mockPublicationDetails(publishViaPartitionRoot) + ), + }) +} + +const TestSection = ({ type }: { type: DestinationType }) => { + const form = useForm({ + defaultValues: { + publicationName: 'analytics', + tableSyncCopyMode: 'include_all_tables', + tableSyncCopyTableIds: [], + maxFillMs: 500, + maxTableSyncWorkers: 4, + maxCopyConnectionsPerTable: 1, + invalidatedSlotBehavior: 'error', + tableOptions: [], + }, + }) + return ( +
+ + + ) +} + +describe('PipelineDataSection', () => { + it.each(['BigQuery', 'DuckLake', 'Snowflake', 'ClickHouse', 'Analytics Bucket'])( + 'groups %s publication, initial sync and replication settings without connection fields', + async (type) => { + mockPublicationRequests(false) + customRender() + expect(screen.getByRole('region', { name: 'Data' })).toBeInTheDocument() + expect(screen.getByText('Initial sync')).toBeInTheDocument() + await waitFor(() => + expect(screen.getByRole('status')).toHaveTextContent( + 'Each partition is replicated separately.' + ) + ) + fireEvent.click(screen.getByRole('button', { name: /Advanced settings/ })) + expect(screen.getByLabelText('Batch wait time')).toHaveValue(500) + expect(screen.getByLabelText('Table sync workers')).toHaveValue(4) + expect(screen.queryByText('Table layout') !== null).toBe(type === 'BigQuery') + for (const label of [ + 'Pipeline name', + 'Pipeline region', + 'Connection pool size', + 'Maximum staleness', + 'Pool size', + 'Role', + ]) { + expect(screen.queryByText(label)).not.toBeInTheDocument() + } + fireEvent.change(screen.getByLabelText('Batch wait time'), { target: { value: '750' } }) + fireEvent.click(screen.getByRole('button', { name: /Advanced settings/ })) + fireEvent.click(screen.getByRole('button', { name: /Advanced settings/ })) + expect(screen.getByLabelText('Batch wait time')).toHaveValue(750) + } + ) +}) diff --git a/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.tsx b/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.tsx new file mode 100644 index 00000000000..934ec566f3f --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.tsx @@ -0,0 +1,28 @@ +import type { UseFormReturn } from 'react-hook-form' + +import { AdvancedSettings } from '../DestinationPanel/DestinationForm/AdvancedSettings' +import type { DestinationPanelSchemaType } from '../DestinationPanel/DestinationForm/DestinationForm.schema' +import { PublicationSelection } from '../DestinationPanel/DestinationForm/PublicationSelection' +import { TableCopySelection } from '../DestinationPanel/DestinationForm/TableCopySelection' +import type { DestinationType } from '../DestinationPanel/DestinationPanel.types' + +export const PipelineDataSection = ({ + type, + form, + onSelectNewPublication, +}: { + type: DestinationType + form: UseFormReturn + onSelectNewPublication: () => void +}) => ( +
+
+

Data

+
+ + +
+
+ +
+) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.test.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.test.tsx index 254ee8371c9..bbfcd09a7c6 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.test.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.test.tsx @@ -317,6 +317,7 @@ describe('DestinationForm edit submission', () => { onClose={vi.fn()} /> ) + expect(screen.queryByRole('region', { name: 'Data' }) !== null).toBe(shouldShowSection) expect(screen.queryByRole('region', { name: 'Destination connection' }) !== null).toBe( shouldShowSection ) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx index b260639232c..88d57dedf69 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx @@ -11,6 +11,7 @@ import { Admonition } from 'ui-patterns/Admonition' import * as z from 'zod' import { PipelineConnectionSection } from '../../CreatePipeline/PipelineConnectionSection' +import { PipelineDataSection } from '../../CreatePipeline/PipelineDataSection' import { useIsETLBigQueryPrivateAlpha, useIsETLClickHousePrivateAlpha, @@ -497,49 +498,45 @@ export const DestinationForm = ({ )} -
- {isErrorExistingConfig && ( - -

- The existing destination or pipeline settings could not be loaded. Refresh - before applying changes. -

-
- )} -

- {shouldShowConnectionSection ? 'Data' : 'Destination details'} -

- -
- {!shouldShowConnectionSection && ( - - )} - setPublicationPanelVisible(true)} - /> - - {!shouldShowConnectionSection && ( - - )} -
-
- - {!shouldShowConnectionSection && ( + {shouldShowConnectionSection ? ( + setPublicationPanelVisible(true)} + /> + ) : ( <> +
+ {isErrorExistingConfig && ( + +

+ The existing destination or pipeline settings could not be loaded. + Refresh before applying changes. +

+
+ )} +

Destination details

+ +
+ + setPublicationPanelVisible(true)} + /> + + +
+
+ {destinationFields} + + + + )} - {!shouldShowConnectionSection && } - - - {!editMode && hasRunValidation && !isValidating && ( <>